source: sasview/src/sas/sasgui/perspectives/fitting/pagestate.py @ 77910cf

ESS_GUIESS_GUI_DocsESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalccostrafo411magnetic_scattrelease-4.1.1release-4.1.2release-4.2.2ticket-1009ticket-1094-headlessticket-1242-2d-resolutionticket-1243ticket-1249ticket885unittest-saveload
Last change on this file since 77910cf was 1c6bad0, checked in by krzywon, 7 years ago

Added patch to handle formfactor and category names that aren't saved in v4.0.1 and earlier. Start on loading str_parameters, but not finished.

  • Property mode set to 100644
File size: 80.5 KB
Line 
1"""
2    Class that holds a fit page state
3"""
4# TODO: Refactor code so we don't need to use getattr/setattr
5################################################################################
6# This software was developed by the University of Tennessee as part of the
7# Distributed Data Analysis of Neutron Scattering Experiments (DANSE)
8# project funded by the US National Science Foundation.
9#
10# See the license text in license.txt
11#
12# copyright 2009, University of Tennessee
13################################################################################
14import time
15import os
16import sys
17import wx
18import copy
19import logging
20import numpy
21import traceback
22
23import xml.dom.minidom
24from xml.dom.minidom import parseString
25from lxml import etree
26
27from sasmodels import convert
28import sasmodels.weights
29
30import sas.sascalc.dataloader
31from sas.sascalc.dataloader.readers.cansas_reader import Reader as CansasReader
32from sas.sascalc.dataloader.readers.cansas_reader import get_content, write_node
33from sas.sascalc.dataloader.data_info import Data2D, Collimation, Detector
34from sas.sascalc.dataloader.data_info import Process, Aperture
35# Information to read/write state as xml
36FITTING_NODE_NAME = 'fitting_plug_in'
37CANSAS_NS = "cansas1d/1.0"
38
39LIST_OF_DATA_ATTRIBUTES = [["is_data", "is_data", "bool"],
40                           ["group_id", "data_group_id", "string"],
41                           ["data_name", "data_name", "string"],
42                           ["data_id", "data_id", "string"],
43                           ["name", "name", "string"],
44                           ["data_name", "data_name", "string"]]
45LIST_OF_STATE_ATTRIBUTES = [["qmin", "qmin", "float"],
46                            ["qmax", "qmax", "float"],
47                            ["npts", "npts", "float"],
48                            ["categorycombobox", "categorycombobox", "string"],
49                            ["formfactorcombobox", "formfactorcombobox",
50                             "string"],
51                            ["structurecombobox", "structurecombobox",
52                             "string"],
53                            ["multi_factor", "multi_factor", "float"],
54                            ["magnetic_on", "magnetic_on", "bool"],
55                            ["enable_smearer", "enable_smearer", "bool"],
56                            ["disable_smearer", "disable_smearer", "bool"],
57                            ["pinhole_smearer", "pinhole_smearer", "bool"],
58                            ["slit_smearer", "slit_smearer", "bool"],
59                            ["enable_disp", "enable_disp", "bool"],
60                            ["disable_disp", "disable_disp", "bool"],
61                            ["dI_noweight", "dI_noweight", "bool"],
62                            ["dI_didata", "dI_didata", "bool"],
63                            ["dI_sqrdata", "dI_sqrdata", "bool"],
64                            ["dI_idata", "dI_idata", "bool"],
65                            ["enable2D", "enable2D", "bool"],
66                            ["cb1", "cb1", "bool"],
67                            ["tcChi", "tcChi", "float"],
68                            ["smearer", "smearer", "float"],
69                            ["smear_type", "smear_type", "string"],
70                            ["dq_l", "dq_l", "float"],
71                            ["dq_r", "dq_r", "float"],
72                            ["dx_max", "dx_max", "float"],
73                            ["dx_min", "dx_min", "float"],
74                            ["dxl", "dxl", "float"],
75                            ["dxw", "dxw", "float"]]
76
77LIST_OF_MODEL_ATTRIBUTES = [["values", "values"],
78                            ["weights", "weights"]]
79
80DISPERSION_LIST = [["disp_obj_dict", "_disp_obj_dict", "string"]]
81
82LIST_OF_STATE_PARAMETERS = [["parameters", "parameters"],
83                            ["str_parameters", "str_parameters"],
84                            ["orientation_parameters", "orientation_params"],
85                            ["dispersity_parameters",
86                             "orientation_params_disp"],
87                            ["fixed_param", "fixed_param"],
88                            ["fittable_param", "fittable_param"]]
89LIST_OF_DATA_2D_ATTR = [["xmin", "xmin", "float"],
90                        ["xmax", "xmax", "float"],
91                        ["ymin", "ymin", "float"],
92                        ["ymax", "ymax", "float"],
93                        ["_xaxis", "_xaxis", "string"],
94                        ["_xunit", "_xunit", "string"],
95                        ["_yaxis", "_yaxis", "string"],
96                        ["_yunit", "_yunit", "string"],
97                        ["_zaxis", "_zaxis", "string"],
98                        ["_zunit", "_zunit", "string"]]
99LIST_OF_DATA_2D_VALUES = [["qx_data", "qx_data", "float"],
100                          ["qy_data", "qy_data", "float"],
101                          ["dqx_data", "dqx_data", "float"],
102                          ["dqy_data", "dqy_data", "float"],
103                          ["data", "data", "float"],
104                          ["q_data", "q_data", "float"],
105                          ["err_data", "err_data", "float"],
106                          ["mask", "mask", "bool"]]
107
108
109def parse_entry_helper(node, item):
110    """
111    Create a numpy list from value extrated from the node
112
113    :param node: node from each the value is stored
114    :param item: list name of three strings.the two first are name of data
115        attribute and the third one is the type of the value of that
116        attribute. type can be string, float, bool, etc.
117
118    : return: numpy array
119    """
120    if node is not None:
121        if item[2] == "string":
122            return str(node.get(item[0]).strip())
123        elif item[2] == "bool":
124            try:
125                return node.get(item[0]).strip() == "True"
126            except Exception:
127                return None
128        else:
129            try:
130                return float(node.get(item[0]))
131            except Exception:
132                return None
133
134
135class PageState(object):
136    """
137    Contains information to reconstruct a page of the fitpanel.
138    """
139    def __init__(self, parent=None, model=None, data=None):
140        """
141        Initialize the current state
142
143        :param model: a selected model within a page
144        :param data:
145
146        """
147        self.file = None
148        # Time of state creation
149        self.timestamp = time.time()
150        # Data member to store the dispersion object created
151        self._disp_obj_dict = {}
152        # ------------------------
153        # Data used for fitting
154        self.data = data
155        # model data
156        self.theory_data = None
157        # Is 2D
158        self.is_2D = False
159        self.images = None
160
161        # save additional information on data that dataloader.reader
162        # does not read
163        self.is_data = None
164        self.data_name = ""
165
166        if self.data is not None:
167            self.data_name = self.data.name
168        self.data_id = None
169        if self.data is not None and hasattr(self.data, "id"):
170            self.data_id = self.data.id
171        self.data_group_id = None
172        if self.data is not None and hasattr(self.data, "group_id"):
173            self.data_group_id = self.data.group_id
174
175        # reset True change the state of existing button
176        self.reset = False
177
178        # flag to allow data2D plot
179        self.enable2D = False
180        # model on which the fit would be performed
181        self.model = model
182        self.m_name = None
183        # list of process done to model
184        self.process = []
185        # fit page manager
186        self.manager = None
187        # Store the parent of this panel parent
188        # For this application fitpanel is the parent
189        self.parent = parent
190        # Event_owner is the owner of model event
191        self.event_owner = None
192        # page name
193        self.page_name = ""
194        # Contains link between model, its parameters, and panel organization
195        self.parameters = []
196        # String parameter list that can not be fitted
197        self.str_parameters = []
198        # Contains list of parameters that cannot be fitted and reference to
199        # panel objects
200        self.fixed_param = []
201        # Contains list of parameters with dispersity and reference to
202        # panel objects
203        self.fittable_param = []
204        # orientation parameters
205        self.orientation_params = []
206        # orientation parameters for gaussian dispersity
207        self.orientation_params_disp = []
208        # smearer info
209        self.smearer = None
210        self.smear_type = None
211        self.dq_l = None
212        self.dq_r = None
213        self.dx_max = None
214        self.dx_min = None
215        self.dxl = None
216        self.dxw = None
217        # list of dispersion parameters
218        self.disp_list = []
219        if self.model is not None:
220            self.disp_list = self.model.getDispParamList()
221
222        self.disp_cb_dict = {}
223        self.values = {}
224        self.weights = {}
225
226        # contains link between a model and selected parameters to fit
227        self.param_toFit = []
228        # dictionary of model type and model class
229        self.model_list_box = None
230        # save the state of the context menu
231        self.saved_states = {}
232        # save selection of combobox
233        self.formfactorcombobox = None
234        self.categorycombobox = None
235        self.structurecombobox = None
236
237        # radio box to select type of model
238        # self.shape_rbutton = False
239        # self.shape_indep_rbutton = False
240        # self.struct_rbutton = False
241        # self.plugin_rbutton = False
242        # the indice of the current selection
243        self.disp_box = 0
244        # Qrange
245        # Q range
246        self.qmin = 0.001
247        self.qmax = 0.1
248        # reset data range
249        self.qmax_x = None
250        self.qmin_x = None
251
252        self.npts = None
253        self.name = ""
254        self.multi_factor = None
255        self.magnetic_on = False
256        # enable smearering state
257        self.enable_smearer = False
258        self.disable_smearer = True
259        self.pinhole_smearer = False
260        self.slit_smearer = False
261        # weighting options
262        self.dI_noweight = False
263        self.dI_didata = True
264        self.dI_sqrdata = False
265        self.dI_idata = False
266        # disperity selection
267        self.enable_disp = False
268        self.disable_disp = True
269
270        # state of selected all check button
271        self.cb1 = False
272        # store value of chisqr
273        self.tcChi = None
274
275    def clone(self):
276        """
277        Create a new copy of the current object
278        """
279        model = None
280        if self.model is not None:
281            model = self.model.clone()
282            model.name = self.model.name
283        obj = PageState(self.parent, model=model)
284        obj.file = copy.deepcopy(self.file)
285        obj.data = copy.deepcopy(self.data)
286        if self.data is not None:
287            self.data_name = self.data.name
288        obj.data_name = self.data_name
289        obj.is_data = self.is_data
290        obj.model_list_box = copy.deepcopy(self.model_list_box)
291
292        obj.categorycombobox = self.categorycombobox
293        obj.formfactorcombobox = self.formfactorcombobox
294        obj.structurecombobox = self.structurecombobox
295
296        # obj.shape_rbutton = self.shape_rbutton
297        # obj.shape_indep_rbutton = self.shape_indep_rbutton
298        # obj.struct_rbutton = self.struct_rbutton
299        # obj.plugin_rbutton = self.plugin_rbutton
300
301        obj.manager = self.manager
302        obj.event_owner = self.event_owner
303        obj.disp_list = copy.deepcopy(self.disp_list)
304
305        obj.enable2D = copy.deepcopy(self.enable2D)
306        obj.parameters = copy.deepcopy(self.parameters)
307        obj.str_parameters = copy.deepcopy(self.str_parameters)
308        obj.fixed_param = copy.deepcopy(self.fixed_param)
309        obj.fittable_param = copy.deepcopy(self.fittable_param)
310        obj.orientation_params = copy.deepcopy(self.orientation_params)
311        obj.orientation_params_disp = \
312            copy.deepcopy(self.orientation_params_disp)
313        obj.enable_disp = copy.deepcopy(self.enable_disp)
314        obj.disable_disp = copy.deepcopy(self.disable_disp)
315        obj.tcChi = self.tcChi
316
317        if len(self._disp_obj_dict) > 0:
318            for k, v in self._disp_obj_dict.iteritems():
319                obj._disp_obj_dict[k] = v
320        if len(self.disp_cb_dict) > 0:
321            for k, v in self.disp_cb_dict.iteritems():
322                obj.disp_cb_dict[k] = v
323        if len(self.values) > 0:
324            for k, v in self.values.iteritems():
325                obj.values[k] = v
326        if len(self.weights) > 0:
327            for k, v in self.weights.iteritems():
328                obj.weights[k] = v
329        obj.enable_smearer = copy.deepcopy(self.enable_smearer)
330        obj.disable_smearer = copy.deepcopy(self.disable_smearer)
331        obj.pinhole_smearer = copy.deepcopy(self.pinhole_smearer)
332        obj.slit_smearer = copy.deepcopy(self.slit_smearer)
333        obj.smear_type = copy.deepcopy(self.smear_type)
334        obj.dI_noweight = copy.deepcopy(self.dI_noweight)
335        obj.dI_didata = copy.deepcopy(self.dI_didata)
336        obj.dI_sqrdata = copy.deepcopy(self.dI_sqrdata)
337        obj.dI_idata = copy.deepcopy(self.dI_idata)
338        obj.dq_l = copy.deepcopy(self.dq_l)
339        obj.dq_r = copy.deepcopy(self.dq_r)
340        obj.dx_max = copy.deepcopy(self.dx_max)
341        obj.dx_min = copy.deepcopy(self.dx_min)
342        obj.dxl = copy.deepcopy(self.dxl)
343        obj.dxw = copy.deepcopy(self.dxw)
344        obj.disp_box = copy.deepcopy(self.disp_box)
345        obj.qmin = copy.deepcopy(self.qmin)
346        obj.qmax = copy.deepcopy(self.qmax)
347        obj.multi_factor = self.multi_factor
348        obj.magnetic_on = self.magnetic_on
349        obj.npts = copy.deepcopy(self.npts)
350        obj.cb1 = copy.deepcopy(self.cb1)
351        obj.smearer = copy.deepcopy(self.smearer)
352
353        for name, state in self.saved_states.iteritems():
354            copy_name = copy.deepcopy(name)
355            copy_state = state.clone()
356            obj.saved_states[copy_name] = copy_state
357        return obj
358
359    def _is_sasmodels(self):
360        """
361        A check to see if the loaded save state was saved in SasView v4_0+
362        :return: None
363        """
364        if self.formfactorcombobox == '':
365            if self.categorycombobox == '' and len(self.parameters) == 3:
366                self.categorycombobox = "Shape-Independent"
367                self.formfactorcombobox = 'PowerLawAbsModel'
368            elif self.categorycombobox == '' and len(self.parameters) == 9:
369                self.categorycombobox = 'Cylinder'
370                self.formfactorcombobox = 'barbell'
371            elif self.categorycombobox == 'Shapes':
372                self.formfactorcombobox = 'BCCrystalModel'
373            elif self.categorycombobox == 'Uncategorized':
374                self.formfactorcombobox = 'LineModel'
375            elif self.categorycombobox == 'StructureFactor':
376                self.structurecombobox = 'HardsphereStructure'
377            elif self.categorycombobox == 'Customized Models':
378                self.formfactorcombobox = 'MySumFunction'
379            elif self.categorycombobox == 'Ellipsoid':
380                self.formfactorcombobox = 'core_shell_ellipsoid'
381            elif self.categorycombobox == 'Lamellae':
382                self.formfactorcombobox = 'lamellar'
383            elif self.categorycombobox == 'Paracrystal':
384                self.formfactorcombobox = 'bcc_paracrystal'
385            elif self.categorycombobox == 'Parallelepiped':
386                self.formfactorcombobox = 'core_shell_parallelepiped'
387            elif self.categorycombobox == 'Shape Independent':
388                self.formfactorcombobox = 'be_polyelectrolyte'
389            elif self.categorycombobox == 'Sphere':
390                self.formfactorcombobox = 'adsorbed_layer'
391            elif self.categorycombobox == 'Structure Factor':
392                self.formfactorcombobox = 'hardsphere'
393        newname = convert._conversion_target(self.formfactorcombobox)
394        if newname == None:
395            return True
396        else:
397            return False
398
399    def param_remap_to_sasmodels_convert(self, params):
400        """
401        Remaps the parameters for sasmodels conversion
402
403        :param params: list of parameters (likely self.parameters)
404        :return: remapped dictionary of parameters
405        """
406        p = dict()
407        for fittable, name, value, _, uncert, lower, upper, units in params:
408            if not value:
409                value = numpy.nan
410            if not uncert or uncert[1] == '':
411                uncert[0] = False
412                uncert[1] = numpy.nan
413            if not upper or upper[1] == '':
414                upper[0] = False
415                upper[1] = numpy.nan
416            if not lower or lower[1] == '':
417                lower[0] = False
418                lower[1] = numpy.nan
419            p[name] = float(value)
420            p[name + ".fittable"] = bool(fittable)
421            p[name + ".std"] = float(uncert[1])
422            p[name + ".upper"] = float(upper[1])
423            p[name + ".lower"] = float(lower[1])
424            p[name + ".units"] = units
425        return p
426
427    def param_remap_from_sasmodels_convert(self, params):
428        """
429        Converts {name : value} map back to [] param list
430        :param params: parameter map returned from sasmodels
431        :return: None
432        """
433        p_map = []
434        for name, info in params.iteritems():
435            if ".fittable" in name or ".std" in name or ".upper" in name or \
436                            ".lower" in name or ".units" in name:
437                pass
438            else:
439                fittable = params.get(name + ".fittable", True)
440                std = params.get(name + ".std", '0.0')
441                upper = params.get(name + ".upper", 'inf')
442                lower = params.get(name + ".lower", '-inf')
443                units = params.get(name + ".units")
444                if std is not None and std is not numpy.nan:
445                    std = [True, str(std)]
446                else:
447                    std = [False, '']
448                if lower is not None and lower is not numpy.nan:
449                    lower = [True, str(lower)]
450                else:
451                    lower = [True, '-inf']
452                if upper is not None and upper is not numpy.nan:
453                    upper = [True, str(upper)]
454                else:
455                    upper = [True, 'inf']
456                param_list = [bool(fittable), str(name), str(info),
457                              "+/-", std, lower, upper, str(units)]
458                p_map.append(param_list)
459        return p_map
460
461    def _convert_to_sasmodels(self):
462        """
463        Convert parameters to a form usable by sasmodels converter
464
465        :return: None
466        """
467        # Create conversion dictionary to send to sasmodels
468        p = self.param_remap_to_sasmodels_convert(self.parameters)
469        structurefactor, params = \
470            convert.convert_model(self.structurecombobox, p)
471        formfactor, params = \
472            convert.convert_model(self.formfactorcombobox, params)
473        if len(self.str_parameters) > 0:
474            str_p = self.param_remap_to_sasmodels_convert(self.str_parameters)
475            formfactor, str_params = convert.convert_model(formfactor, str_p)
476        else:
477            str_params = None
478
479        # Only convert if old != new, otherwise all the same
480        if formfactor != self.formfactorcombobox or \
481                        structurefactor != self.structurecombobox:
482            self.formfactorcombobox = formfactor
483            self.structurecombobox = structurefactor
484            self.parameters = []
485            self.parameters = self.param_remap_from_sasmodels_convert(params)
486            if str_params:
487                self.str_parameters = \
488                    self.param_remap_from_sasmodels_convert(str_params)
489
490    def _repr_helper(self, list, rep):
491        """
492        Helper method to print a state
493        """
494        for item in list:
495            rep += "parameter name: %s \n" % str(item[1])
496            rep += "value: %s\n" % str(item[2])
497            rep += "selected: %s\n" % str(item[0])
498            rep += "error displayed : %s \n" % str(item[4][0])
499            rep += "error value:%s \n" % str(item[4][1])
500            rep += "minimum displayed : %s \n" % str(item[5][0])
501            rep += "minimum value : %s \n" % str(item[5][1])
502            rep += "maximum displayed : %s \n" % str(item[6][0])
503            rep += "maximum value : %s \n" % str(item[6][1])
504            rep += "parameter unit: %s\n\n" % str(item[7])
505        return rep
506
507    def __repr__(self):
508        """
509        output string for printing
510        """
511        rep = "\nState name: %s\n" % self.file
512        t = time.localtime(self.timestamp)
513        time_str = time.strftime("%b %d %Y %H;%M;%S ", t)
514
515        rep += "State created: %s\n" % time_str
516        rep += "State form factor combobox selection: %s\n" % \
517               self.formfactorcombobox
518        rep += "State structure factor combobox selection: %s\n" % \
519               self.structurecombobox
520        rep += "is data : %s\n" % self.is_data
521        rep += "data's name : %s\n" % self.data_name
522        rep += "data's id : %s\n" % self.data_id
523        if self.model is not None:
524            m_name = self.model.__class__.__name__
525            if m_name == 'Model':
526                m_name = self.m_name
527            rep += "model name : %s\n" % m_name
528        else:
529            rep += "model name : None\n"
530        rep += "multi_factor : %s\n" % str(self.multi_factor)
531        rep += "magnetic_on : %s\n" % str(self.magnetic_on)
532        rep += "model type (Category) selected: %s\n" % self.categorycombobox
533        rep += "data : %s\n" % str(self.data)
534        rep += "Plotting Range: min: %s, max: %s, steps: %s\n" % \
535               (str(self.qmin), str(self.qmax), str(self.npts))
536        rep += "Dispersion selection : %s\n" % str(self.disp_box)
537        rep += "Smearing enable : %s\n" % str(self.enable_smearer)
538        rep += "Smearing disable : %s\n" % str(self.disable_smearer)
539        rep += "Pinhole smearer enable : %s\n" % str(self.pinhole_smearer)
540        rep += "Slit smearer enable : %s\n" % str(self.slit_smearer)
541        rep += "Dispersity enable : %s\n" % str(self.enable_disp)
542        rep += "Dispersity disable : %s\n" % str(self.disable_disp)
543        rep += "Slit smearer enable: %s\n" % str(self.slit_smearer)
544
545        rep += "dI_noweight : %s\n" % str(self.dI_noweight)
546        rep += "dI_didata : %s\n" % str(self.dI_didata)
547        rep += "dI_sqrdata : %s\n" % str(self.dI_sqrdata)
548        rep += "dI_idata : %s\n" % str(self.dI_idata)
549
550        rep += "2D enable : %s\n" % str(self.enable2D)
551        rep += "All parameters checkbox selected: %s\n" % self.cb1
552        rep += "Value of Chisqr : %s\n" % str(self.tcChi)
553        rep += "Smear object : %s\n" % self.smearer
554        rep += "Smear type : %s\n" % self.smear_type
555        rep += "dq_l  : %s\n" % self.dq_l
556        rep += "dq_r  : %s\n" % self.dq_r
557        rep += "dx_max  : %s\n" % str(self.dx_max)
558        rep += "dx_min : %s\n" % str(self.dx_min)
559        rep += "dxl  : %s\n" % str(self.dxl)
560        rep += "dxw : %s\n" % str(self.dxw)
561        rep += "model  : %s\n\n" % str(self.model)
562        temp_parameters = []
563        temp_fittable_param = []
564        if self.data.__class__.__name__ == "Data2D":
565            self.is_2D = True
566        else:
567            self.is_2D = False
568        if self.data is not None:
569            if not self.is_2D:
570                for item in self.parameters:
571                    if item not in self.orientation_params:
572                        temp_parameters.append(item)
573                for item in self.fittable_param:
574                    if item not in self.orientation_params_disp:
575                        temp_fittable_param.append(item)
576            else:
577                temp_parameters = self.parameters
578                temp_fittable_param = self.fittable_param
579
580            rep += "number parameters(self.parameters): %s\n" % \
581                   len(temp_parameters)
582            rep = self._repr_helper(list=temp_parameters, rep=rep)
583            rep += "number str_parameters(self.str_parameters): %s\n" % \
584                   len(self.str_parameters)
585            rep = self._repr_helper(list=self.str_parameters, rep=rep)
586            rep += "number fittable_param(self.fittable_param): %s\n" % \
587                   len(temp_fittable_param)
588            rep = self._repr_helper(list=temp_fittable_param, rep=rep)
589        return rep
590
591    def set_report_string(self):
592        """
593        Get the values (strings) from __str__ for report
594        """
595        # Dictionary of the report strings
596        repo_time = ""
597        model_name = ""
598        title = ""
599        title_name = ""
600        file_name = ""
601        param_string = ""
602        paramval_string = ""
603        chi2_string = ""
604        q_range = ""
605        strings = self.__repr__()
606        lines = strings.split('\n')
607
608        # get all string values from __str__()
609        for line in lines:
610            value = ""
611            content = line.split(":")
612            name = content[0]
613            try:
614                value = content[1]
615            except Exception:
616                msg = "Report string expected 'name: value' but got %r" % line
617                logging.error(msg)
618            if name.count("State created"):
619                repo_time = "" + value
620            if name.count("parameter name"):
621                val_name = value.split(".")
622                if len(val_name) > 1:
623                    if val_name[1].count("width"):
624                        param_string += value + ','
625                    else:
626                        continue
627                else:
628                    param_string += value + ','
629            if name == "value":
630                param_string += value + ','
631            fixed_parameter = False
632            if name == "selected":
633                if value == u' False':
634                    fixed_parameter = True
635            if name == "error value":
636                if fixed_parameter:
637                    param_string += '(fixed),'
638                else:
639                    param_string += value + ','
640            if name == "parameter unit":
641                param_string += value + ':'
642            if name == "Value of Chisqr ":
643                chi2 = ("Chi2/Npts = " + value)
644                chi2_string = CENTRE % chi2
645            if name == "Title":
646                if len(value.strip()) == 0:
647                    continue
648                title = value + " [" + repo_time + "]"
649                title_name = HEADER % title
650            if name == "data ":
651                try:
652                    file_value = ("File name:" + content[2])
653                    file_name = CENTRE % file_value
654                    if len(title) == 0:
655                        title = content[2] + " [" + repo_time + "]"
656                        title_name = HEADER % title
657                except Exception:
658                    msg = "While parsing 'data: ...'\n"
659                    logging.error(msg + traceback.format_exc())
660            if name == "model name ":
661                try:
662                    modelname = "Model name:" + content[1]
663                except:
664                    modelname = "Model name:" + " NAN"
665                model_name = CENTRE % modelname
666
667            if name == "Plotting Range":
668                try:
669                    q_range = content[1] + " = " + content[2] \
670                            + " = " + content[3].split(",")[0]
671                    q_name = ("Q Range:    " + q_range)
672                    q_range = CENTRE % q_name
673                except Exception:
674                    msg = "While parsing 'Plotting Range: ...'\n"
675                    logging.error(msg + traceback.format_exc())
676        paramval = ""
677        for lines in param_string.split(":"):
678            line = lines.split(",")
679            if len(lines) > 0:
680                param = line[0]
681                param += " = " + line[1]
682                if len(line[2].split()) > 0 and not line[2].count("None"):
683                    param += " +- " + line[2]
684                if len(line[3].split()) > 0 and not line[3].count("None"):
685                    param += " " + line[3]
686                if not paramval.count(param):
687                    paramval += param + "\n"
688                    paramval_string += CENTRE % param + "\n"
689
690        text_string = "\n\n\n%s\n\n%s\n%s\n%s\n\n%s" % \
691                      (title, file, q_name, chi2, paramval)
692
693        title_name = self._check_html_format(title_name)
694        file_name = self._check_html_format(file_name)
695        title = self._check_html_format(title)
696
697        html_string = title_name + "\n" + file_name + \
698                                   "\n" + model_name + \
699                                   "\n" + q_range + \
700                                   "\n" + chi2_string + \
701                                   "\n" + ELINE + \
702                                   "\n" + paramval_string + \
703                                   "\n" + ELINE + \
704                                   "\n" + FEET_1 % title + \
705                                   "\n" + FEET_2
706
707        return html_string, text_string, title
708
709    def _check_html_format(self, name):
710        """
711        Check string '%' for html format
712        """
713        if name.count('%'):
714            name = name.replace('%', '&#37')
715
716        return name
717
718    def report(self, figs=None, canvases=None):
719        """
720        Invoke report dialog panel
721
722        : param figs: list of pylab figures [list]
723        """
724        from sas.sasgui.perspectives.fitting.report_dialog import ReportDialog
725        # get the strings for report
726        html_str, text_str, title = self.set_report_string()
727        # Allow 2 figures to append
728        if len(figs) == 1:
729            add_str = FEET_3
730        elif len(figs) == 2:
731            add_str = ELINE
732            add_str += FEET_2 % ("%s")
733            add_str += ELINE
734            add_str += FEET_3
735        elif len(figs) > 2:
736            add_str = ELINE
737            add_str += FEET_2 % ("%s")
738            add_str += ELINE
739            add_str += FEET_2 % ("%s")
740            add_str += ELINE
741            add_str += FEET_3
742        else:
743            add_str = ""
744
745        # final report html strings
746        report_str = html_str % ("%s") + add_str
747
748        # make plot image
749        images = self.set_plot_state(figs, canvases)
750        report_list = [report_str, text_str, images]
751        dialog = ReportDialog(report_list, None, wx.ID_ANY, "")
752        dialog.Show()
753
754    def _to_xml_helper(self, thelist, element, newdoc):
755        """
756        Helper method to create xml file for saving state
757        """
758        for item in thelist:
759            sub_element = newdoc.createElement('parameter')
760            sub_element.setAttribute('name', str(item[1]))
761            sub_element.setAttribute('value', str(item[2]))
762            sub_element.setAttribute('selected_to_fit', str(item[0]))
763            sub_element.setAttribute('error_displayed', str(item[4][0]))
764            sub_element.setAttribute('error_value', str(item[4][1]))
765            sub_element.setAttribute('minimum_displayed', str(item[5][0]))
766            sub_element.setAttribute('minimum_value', str(item[5][1]))
767            sub_element.setAttribute('maximum_displayed', str(item[6][0]))
768            sub_element.setAttribute('maximum_value', str(item[6][1]))
769            sub_element.setAttribute('unit', str(item[7]))
770            element.appendChild(sub_element)
771
772    def to_xml(self, file="fitting_state.fitv", doc=None,
773               entry_node=None, batch_fit_state=None):
774        """
775        Writes the state of the fit panel to file, as XML.
776
777        Compatible with standalone writing, or appending to an
778        already existing XML document. In that case, the XML document is
779        required. An optional entry node in the XML document may also be given.
780
781        :param file: file to write to
782        :param doc: XML document object [optional]
783        :param entry_node: XML node within the XML document at which we
784                           will append the data [optional]
785        :param batch_fit_state: simultaneous fit state
786        """
787        from xml.dom.minidom import getDOMImplementation
788
789        # Check whether we have to write a standalone XML file
790        if doc is None:
791            impl = getDOMImplementation()
792            doc_type = impl.createDocumentType(FITTING_NODE_NAME, "1.0", "1.0")
793            newdoc = impl.createDocument(None, FITTING_NODE_NAME, doc_type)
794            top_element = newdoc.documentElement
795        else:
796            # We are appending to an existing document
797            newdoc = doc
798            try:
799                top_element = newdoc.createElement(FITTING_NODE_NAME)
800            except:
801                string = etree.tostring(doc, pretty_print=True)
802                newdoc = parseString(string)
803                top_element = newdoc.createElement(FITTING_NODE_NAME)
804            if entry_node is None:
805                newdoc.documentElement.appendChild(top_element)
806            else:
807                try:
808                    entry_node.appendChild(top_element)
809                except:
810                    node_name = entry_node.tag
811                    node_list = newdoc.getElementsByTagName(node_name)
812                    entry_node = node_list.item(0)
813                    entry_node.appendChild(top_element)
814
815        attr = newdoc.createAttribute("version")
816        attr.nodeValue = '1.0'
817        top_element.setAttributeNode(attr)
818
819        # File name
820        element = newdoc.createElement("filename")
821        if self.file is not None:
822            element.appendChild(newdoc.createTextNode(str(self.file)))
823        else:
824            element.appendChild(newdoc.createTextNode(str(file)))
825        top_element.appendChild(element)
826
827        element = newdoc.createElement("timestamp")
828        element.appendChild(newdoc.createTextNode(time.ctime(self.timestamp)))
829        attr = newdoc.createAttribute("epoch")
830        attr.nodeValue = str(self.timestamp)
831        element.setAttributeNode(attr)
832        top_element.appendChild(element)
833
834        # Inputs
835        inputs = newdoc.createElement("Attributes")
836        top_element.appendChild(inputs)
837
838        if self.data is not None and hasattr(self.data, "group_id"):
839            self.data_group_id = self.data.group_id
840        if self.data is not None and hasattr(self.data, "is_data"):
841            self.is_data = self.data.is_data
842        if self.data is not None:
843            self.data_name = self.data.name
844        if self.data is not None and hasattr(self.data, "id"):
845            self.data_id = self.data.id
846
847        for item in LIST_OF_DATA_ATTRIBUTES:
848            element = newdoc.createElement(item[0])
849            element.setAttribute(item[0], str(getattr(self, item[1])))
850            inputs.appendChild(element)
851
852        for item in LIST_OF_STATE_ATTRIBUTES:
853            element = newdoc.createElement(item[0])
854            element.setAttribute(item[0], str(getattr(self, item[1])))
855            inputs.appendChild(element)
856
857        # For self.values ={ disp_param_name: [vals,...],...}
858        # and for self.weights ={ disp_param_name: [weights,...],...}
859        for item in LIST_OF_MODEL_ATTRIBUTES:
860            element = newdoc.createElement(item[0])
861            value_list = getattr(self, item[1])
862            for key, value in value_list.iteritems():
863                sub_element = newdoc.createElement(key)
864                sub_element.setAttribute('name', str(key))
865                for val in value:
866                    sub_element.appendChild(newdoc.createTextNode(str(val)))
867
868                element.appendChild(sub_element)
869            inputs.appendChild(element)
870
871        # Create doc for the dictionary of self._disp_obj_dic
872        for tagname, varname, tagtype in DISPERSION_LIST:
873            element = newdoc.createElement(tagname)
874            value_list = getattr(self, varname)
875            for key, value in value_list.iteritems():
876                sub_element = newdoc.createElement(key)
877                sub_element.setAttribute('name', str(key))
878                sub_element.setAttribute('value', str(value))
879                element.appendChild(sub_element)
880            inputs.appendChild(element)
881
882        for item in LIST_OF_STATE_PARAMETERS:
883            element = newdoc.createElement(item[0])
884            self._to_xml_helper(thelist=getattr(self, item[1]),
885                                element=element, newdoc=newdoc)
886            inputs.appendChild(element)
887
888        # Combined and Simultaneous Fit Parameters
889        if batch_fit_state is not None:
890            batch_combo = newdoc.createElement('simultaneous_fit')
891            top_element.appendChild(batch_combo)
892
893            # Simultaneous Fit Number For Linking Later
894            element = newdoc.createElement('sim_fit_number')
895            element.setAttribute('fit_number', str(batch_fit_state.fit_page_no))
896            batch_combo.appendChild(element)
897
898            # Save constraints
899            constraints = newdoc.createElement('constraints')
900            batch_combo.appendChild(constraints)
901            for constraint in batch_fit_state.constraints_list:
902                if constraint.model_cbox.GetValue() != "":
903                    # model_cbox, param_cbox, egal_txt, constraint,
904                    # btRemove, sizer
905                    doc_cons = newdoc.createElement('constraint')
906                    doc_cons.setAttribute('model_cbox',
907                                          str(constraint.model_cbox.GetValue()))
908                    doc_cons.setAttribute('param_cbox',
909                                          str(constraint.param_cbox.GetValue()))
910                    doc_cons.setAttribute('egal_txt',
911                                          str(constraint.egal_txt.GetLabel()))
912                    doc_cons.setAttribute('constraint',
913                                          str(constraint.constraint.GetValue()))
914                    constraints.appendChild(doc_cons)
915
916            # Save all models
917            models = newdoc.createElement('model_list')
918            batch_combo.appendChild(models)
919            for model in batch_fit_state.model_list:
920                doc_model = newdoc.createElement('model_list_item')
921                doc_model.setAttribute('checked', str(model[0].GetValue()))
922                keys = model[1].keys()
923                doc_model.setAttribute('name', str(keys[0]))
924                values = model[1].get(keys[0])
925                doc_model.setAttribute('fit_number', str(model[2]))
926                doc_model.setAttribute('fit_page_source', str(model[3]))
927                doc_model.setAttribute('model_name', str(values.model.id))
928                models.appendChild(doc_model)
929
930            # Select All Checkbox
931            element = newdoc.createElement('select_all')
932            if batch_fit_state.select_all:
933                element.setAttribute('checked', 'True')
934            else:
935                element.setAttribute('checked', 'False')
936            batch_combo.appendChild(element)
937
938        # Save the file
939        if doc is None:
940            fd = open(file, 'w')
941            fd.write(newdoc.toprettyxml())
942            fd.close()
943            return None
944        else:
945            return newdoc
946
947    def _from_xml_helper(self, node, list):
948        """
949        Helper function to write state to xml
950        """
951        for item in node:
952            try:
953                name = item.get('name')
954            except:
955                name = None
956            try:
957                value = item.get('value')
958            except:
959                value = None
960            try:
961                selected_to_fit = (item.get('selected_to_fit') == "True")
962            except:
963                selected_to_fit = None
964            try:
965                error_displayed = (item.get('error_displayed') == "True")
966            except:
967                error_displayed = None
968            try:
969                error_value = item.get('error_value')
970            except:
971                error_value = None
972            try:
973                minimum_displayed = (item.get('minimum_displayed') == "True")
974            except:
975                minimum_displayed = None
976            try:
977                minimum_value = item.get('minimum_value')
978            except:
979                minimum_value = None
980            try:
981                maximum_displayed = (item.get('maximum_displayed') == "True")
982            except:
983                maximum_displayed = None
984            try:
985                maximum_value = item.get('maximum_value')
986            except:
987                maximum_value = None
988            try:
989                unit = item.get('unit')
990            except:
991                unit = None
992            list.append([selected_to_fit, name, value, "+/-",
993                         [error_displayed, error_value],
994                         [minimum_displayed, minimum_value],
995                         [maximum_displayed, maximum_value], unit])
996
997    def from_xml(self, file=None, node=None):
998        """
999        Load fitting state from a file
1000
1001        :param file: .fitv file
1002        :param node: node of a XML document to read from
1003        """
1004        if file is not None:
1005            msg = "PageState no longer supports non-CanSAS"
1006            msg += " format for fitting files"
1007            raise RuntimeError, msg
1008
1009        if node.get('version') and node.get('version') == '1.0':
1010
1011            # Get file name
1012            entry = get_content('ns:filename', node)
1013            if entry is not None:
1014                self.file = entry.text.strip()
1015
1016            # Get time stamp
1017            entry = get_content('ns:timestamp', node)
1018            if entry is not None and entry.get('epoch'):
1019                try:
1020                    self.timestamp = float(entry.get('epoch'))
1021                except:
1022                    msg = "PageState.fromXML: Could not"
1023                    msg += " read timestamp\n %s" % sys.exc_value
1024                    logging.error(msg)
1025
1026            if entry is not None:
1027                # Parse fitting attributes
1028                entry = get_content('ns:Attributes', node)
1029                for item in LIST_OF_DATA_ATTRIBUTES:
1030                    node = get_content('ns:%s' % item[0], entry)
1031                    setattr(self, item[0], parse_entry_helper(node, item))
1032
1033                for item in LIST_OF_STATE_ATTRIBUTES:
1034                    node = get_content('ns:%s' % item[0], entry)
1035                    setattr(self, item[0], parse_entry_helper(node, item))
1036
1037                for item in LIST_OF_STATE_PARAMETERS:
1038                    node = get_content("ns:%s" % item[0], entry)
1039                    self._from_xml_helper(node=node,
1040                                          list=getattr(self, item[1]))
1041
1042                # Recover _disp_obj_dict from xml file
1043                self._disp_obj_dict = {}
1044                for tagname, varname, tagtype in DISPERSION_LIST:
1045                    node = get_content("ns:%s" % tagname, entry)
1046                    for attr in node:
1047                        parameter = str(attr.get('name'))
1048                        value = attr.get('value')
1049                        if value.startswith("<"):
1050                            try:
1051                                # <path.to.NamedDistribution object/instance...>
1052                                cls_name = value[1:].split()[0].split('.')[-1]
1053                                cls = getattr(sasmodels.weights, cls_name)
1054                                value = cls.type
1055                            except Exception:
1056                                base = "unable to load distribution %r for %s"
1057                                logging.error(base % (value, parameter))
1058                                continue
1059                        _disp_obj_dict = getattr(self, varname)
1060                        _disp_obj_dict[parameter] = value
1061
1062                # get self.values and self.weights dic. if exists
1063                for tagname, varname in LIST_OF_MODEL_ATTRIBUTES:
1064                    node = get_content("ns:%s" % tagname, entry)
1065                    dic = {}
1066                    value_list = []
1067                    for par in node:
1068                        name = par.get('name')
1069                        values = par.text.split()
1070                        # Get lines only with numbers
1071                        for line in values:
1072                            try:
1073                                val = float(line)
1074                                value_list.append(val)
1075                            except Exception:
1076                                # pass if line is empty (it happens)
1077                                msg = ("Error reading %r from %s %s\n"
1078                                       % (line, tagname, name))
1079                                logging.error(msg + traceback.format_exc())
1080                        dic[name] = numpy.array(value_list)
1081                    setattr(self, varname, dic)
1082
1083    def set_plot_state(self, figs, canvases):
1084        """
1085        Build image state that wx.html understand
1086        by plotting, putting it into wx.FileSystem image object
1087
1088        """
1089        images = []
1090
1091        # Reset memory
1092        self.imgRAM = None
1093        wx.MemoryFSHandler()
1094
1095        # For no figures in the list, prepare empty plot
1096        if figs is None or len(figs) == 0:
1097            figs = [None]
1098
1099        # Loop over the list of figures
1100        # use wx.MemoryFSHandler
1101        self.imgRAM = wx.MemoryFSHandler()
1102        for fig in figs:
1103            if fig is not None:
1104                ind = figs.index(fig)
1105                canvas = canvases[ind]
1106
1107            # store the image in wx.FileSystem Object
1108            wx.FileSystem.AddHandler(wx.MemoryFSHandler())
1109
1110            # index of the fig
1111            ind = figs.index(fig)
1112
1113            # AddFile, image can be retrieved with 'memory:filename'
1114            self.imgRAM.AddFile('img_fit%s.png' % ind,
1115                                canvas.bitmap, wx.BITMAP_TYPE_PNG)
1116
1117            # append figs
1118            images.append(fig)
1119
1120        return images
1121
1122
1123class Reader(CansasReader):
1124    """
1125    Class to load a .fitv fitting file
1126    """
1127    # File type
1128    type_name = "Fitting"
1129
1130    # Wildcards
1131    type = ["Fitting files (*.fitv)|*.fitv"
1132            "SASView file (*.svs)|*.svs"]
1133    # List of allowed extensions
1134    ext = ['.fitv', '.FITV', '.svs', 'SVS']
1135
1136    def __init__(self, call_back=None, cansas=True):
1137        CansasReader.__init__(self)
1138        """
1139        Initialize the call-back method to be called
1140        after we load a file
1141
1142        :param call_back: call-back method
1143        :param cansas:  True = files will be written/read in CanSAS format
1144                        False = write CanSAS format
1145
1146        """
1147        # Call back method to be executed after a file is read
1148        self.call_back = call_back
1149        # CanSAS format flag
1150        self.cansas = cansas
1151        self.state = None
1152        # batch fitting params for saving
1153        self.batchfit_params = []
1154
1155    def get_state(self):
1156        return self.state
1157
1158    def read(self, path):
1159        """
1160        Load a new P(r) inversion state from file
1161
1162        :param path: file path
1163
1164        """
1165        if self.cansas:
1166            return self._read_cansas(path)
1167
1168    def _data2d_to_xml_doc(self, datainfo):
1169        """
1170        Create an XML document to contain the content of a Data2D
1171
1172        :param datainfo: Data2D object
1173
1174        """
1175        if not issubclass(datainfo.__class__, Data2D):
1176            raise RuntimeError, "The cansas writer expects a Data2D instance"
1177
1178        title = "cansas1d/%s" % self.version
1179        title += "http://svn.smallangles.net/svn/canSAS/1dwg/trunk/cansas1d.xsd"
1180        doc = xml.dom.minidom.Document()
1181        main_node = doc.createElement("SASroot")
1182        main_node.setAttribute("version", self.version)
1183        main_node.setAttribute("xmlns", "cansas1d/%s" % self.version)
1184        main_node.setAttribute("xmlns:xsi",
1185                               "http://www.w3.org/2001/XMLSchema-instance")
1186        main_node.setAttribute("xsi:schemaLocation", title)
1187
1188        doc.appendChild(main_node)
1189
1190        entry_node = doc.createElement("SASentry")
1191        main_node.appendChild(entry_node)
1192
1193        write_node(doc, entry_node, "Title", datainfo.title)
1194        if datainfo is not None:
1195            write_node(doc, entry_node, "data_class",
1196                       datainfo.__class__.__name__)
1197        for item in datainfo.run:
1198            runname = {}
1199            if item in datainfo.run_name and \
1200                            len(str(datainfo.run_name[item])) > 1:
1201                runname = {'name': datainfo.run_name[item]}
1202            write_node(doc, entry_node, "Run", item, runname)
1203        # Data info
1204        new_node = doc.createElement("SASdata")
1205        entry_node.appendChild(new_node)
1206        for item in LIST_OF_DATA_2D_ATTR:
1207            element = doc.createElement(item[0])
1208            element.setAttribute(item[0], str(getattr(datainfo, item[1])))
1209            new_node.appendChild(element)
1210
1211        for item in LIST_OF_DATA_2D_VALUES:
1212            root_node = doc.createElement(item[0])
1213            new_node.appendChild(root_node)
1214            temp_list = getattr(datainfo, item[1])
1215
1216            if temp_list is None or len(temp_list) == 0:
1217                element = doc.createElement(item[0])
1218                element.appendChild(doc.createTextNode(str(temp_list)))
1219                root_node.appendChild(element)
1220            else:
1221                for value in temp_list:
1222                    element = doc.createElement(item[0])
1223                    element.setAttribute(item[0], str(value))
1224                    root_node.appendChild(element)
1225
1226        # Sample info
1227        sample = doc.createElement("SASsample")
1228        if datainfo.sample.name is not None:
1229            sample.setAttribute("name", str(datainfo.sample.name))
1230        entry_node.appendChild(sample)
1231        write_node(doc, sample, "ID", str(datainfo.sample.ID))
1232        write_node(doc, sample, "thickness", datainfo.sample.thickness,
1233                   {"unit": datainfo.sample.thickness_unit})
1234        write_node(doc, sample, "transmission", datainfo.sample.transmission)
1235        write_node(doc, sample, "temperature", datainfo.sample.temperature,
1236                   {"unit": datainfo.sample.temperature_unit})
1237
1238        for item in datainfo.sample.details:
1239            write_node(doc, sample, "details", item)
1240
1241        pos = doc.createElement("position")
1242        written = write_node(doc, pos, "x", datainfo.sample.position.x,
1243                             {"unit": datainfo.sample.position_unit})
1244        written = written | write_node(doc, pos, "y",
1245                                       datainfo.sample.position.y,
1246                                       {"unit": datainfo.sample.position_unit})
1247        written = written | write_node(doc, pos, "z",
1248                                       datainfo.sample.position.z,
1249                                       {"unit": datainfo.sample.position_unit})
1250        if written:
1251            sample.appendChild(pos)
1252
1253        ori = doc.createElement("orientation")
1254        written = write_node(doc, ori, "roll", datainfo.sample.orientation.x,
1255                             {"unit": datainfo.sample.orientation_unit})
1256        written = written | write_node(doc, ori, "pitch",
1257                                       datainfo.sample.orientation.y,
1258                                       {"unit":
1259                                            datainfo.sample.orientation_unit})
1260        written = written | write_node(doc, ori, "yaw",
1261                                       datainfo.sample.orientation.z,
1262                                       {"unit":
1263                                            datainfo.sample.orientation_unit})
1264        if written:
1265            sample.appendChild(ori)
1266
1267        # Instrument info
1268        instr = doc.createElement("SASinstrument")
1269        entry_node.appendChild(instr)
1270
1271        write_node(doc, instr, "name", datainfo.instrument)
1272
1273        #   Source
1274        source = doc.createElement("SASsource")
1275        if datainfo.source.name is not None:
1276            source.setAttribute("name", str(datainfo.source.name))
1277        instr.appendChild(source)
1278
1279        write_node(doc, source, "radiation", datainfo.source.radiation)
1280        write_node(doc, source, "beam_shape", datainfo.source.beam_shape)
1281        size = doc.createElement("beam_size")
1282        if datainfo.source.beam_size_name is not None:
1283            size.setAttribute("name", str(datainfo.source.beam_size_name))
1284        written = write_node(doc, size, "x", datainfo.source.beam_size.x,
1285                             {"unit": datainfo.source.beam_size_unit})
1286        written = written | write_node(doc, size, "y",
1287                                       datainfo.source.beam_size.y,
1288                                       {"unit": datainfo.source.beam_size_unit})
1289        written = written | write_node(doc, size, "z",
1290                                       datainfo.source.beam_size.z,
1291                                       {"unit": datainfo.source.beam_size_unit})
1292        if written:
1293            source.appendChild(size)
1294
1295        write_node(doc, source, "wavelength", datainfo.source.wavelength,
1296                   {"unit": datainfo.source.wavelength_unit})
1297        write_node(doc, source, "wavelength_min",
1298                   datainfo.source.wavelength_min,
1299                   {"unit": datainfo.source.wavelength_min_unit})
1300        write_node(doc, source, "wavelength_max",
1301                   datainfo.source.wavelength_max,
1302                   {"unit": datainfo.source.wavelength_max_unit})
1303        write_node(doc, source, "wavelength_spread",
1304                   datainfo.source.wavelength_spread,
1305                   {"unit": datainfo.source.wavelength_spread_unit})
1306
1307        #   Collimation
1308        for item in datainfo.collimation:
1309            coll = doc.createElement("SAScollimation")
1310            if item.name is not None:
1311                coll.setAttribute("name", str(item.name))
1312            instr.appendChild(coll)
1313
1314            write_node(doc, coll, "length", item.length,
1315                       {"unit": item.length_unit})
1316
1317            for apert in item.aperture:
1318                ap = doc.createElement("aperture")
1319                if apert.name is not None:
1320                    ap.setAttribute("name", str(apert.name))
1321                if apert.type is not None:
1322                    ap.setAttribute("type", str(apert.type))
1323                coll.appendChild(ap)
1324
1325                write_node(doc, ap, "distance", apert.distance,
1326                           {"unit": apert.distance_unit})
1327
1328                size = doc.createElement("size")
1329                if apert.size_name is not None:
1330                    size.setAttribute("name", str(apert.size_name))
1331                written = write_node(doc, size, "x", apert.size.x,
1332                                     {"unit": apert.size_unit})
1333                written = written | write_node(doc, size, "y", apert.size.y,
1334                                               {"unit": apert.size_unit})
1335                written = written | write_node(doc, size, "z", apert.size.z,
1336                                               {"unit": apert.size_unit})
1337                if written:
1338                    ap.appendChild(size)
1339
1340        #   Detectors
1341        for item in datainfo.detector:
1342            det = doc.createElement("SASdetector")
1343            written = write_node(doc, det, "name", item.name)
1344            written = written | write_node(doc, det, "SDD", item.distance,
1345                                           {"unit": item.distance_unit})
1346            written = written | write_node(doc, det, "slit_length",
1347                                           item.slit_length,
1348                                           {"unit": item.slit_length_unit})
1349            if written:
1350                instr.appendChild(det)
1351
1352            off = doc.createElement("offset")
1353            written = write_node(doc, off, "x", item.offset.x,
1354                                 {"unit": item.offset_unit})
1355            written = written | write_node(doc, off, "y", item.offset.y,
1356                                           {"unit": item.offset_unit})
1357            written = written | write_node(doc, off, "z", item.offset.z,
1358                                           {"unit": item.offset_unit})
1359            if written:
1360                det.appendChild(off)
1361
1362            center = doc.createElement("beam_center")
1363            written = write_node(doc, center, "x", item.beam_center.x,
1364                                 {"unit": item.beam_center_unit})
1365            written = written | write_node(doc, center, "y",
1366                                           item.beam_center.y,
1367                                           {"unit": item.beam_center_unit})
1368            written = written | write_node(doc, center, "z",
1369                                           item.beam_center.z,
1370                                           {"unit": item.beam_center_unit})
1371            if written:
1372                det.appendChild(center)
1373
1374            pix = doc.createElement("pixel_size")
1375            written = write_node(doc, pix, "x", item.pixel_size.x,
1376                                 {"unit": item.pixel_size_unit})
1377            written = written | write_node(doc, pix, "y", item.pixel_size.y,
1378                                           {"unit": item.pixel_size_unit})
1379            written = written | write_node(doc, pix, "z", item.pixel_size.z,
1380                                           {"unit": item.pixel_size_unit})
1381            if written:
1382                det.appendChild(pix)
1383
1384            ori = doc.createElement("orientation")
1385            written = write_node(doc, ori, "roll", item.orientation.x,
1386                                 {"unit": item.orientation_unit})
1387            written = written | write_node(doc, ori, "pitch",
1388                                           item.orientation.y,
1389                                           {"unit": item.orientation_unit})
1390            written = written | write_node(doc, ori, "yaw", item.orientation.z,
1391                                           {"unit": item.orientation_unit})
1392            if written:
1393                det.appendChild(ori)
1394
1395        # Processes info
1396        for item in datainfo.process:
1397            node = doc.createElement("SASprocess")
1398            entry_node.appendChild(node)
1399
1400            write_node(doc, node, "name", item.name)
1401            write_node(doc, node, "date", item.date)
1402            write_node(doc, node, "description", item.description)
1403            for term in item.term:
1404                value = term['value']
1405                del term['value']
1406                write_node(doc, node, "term", value, term)
1407            for note in item.notes:
1408                write_node(doc, node, "SASprocessnote", note)
1409        # Return the document, and the SASentry node associated with
1410        # the data we just wrote
1411        return doc, entry_node
1412
1413    def _parse_state(self, entry):
1414        """
1415        Read a fit result from an XML node
1416
1417        :param entry: XML node to read from
1418        :return: PageState object
1419        """
1420        # Create an empty state
1421        state = None
1422        # Locate the P(r) node
1423        try:
1424            nodes = entry.xpath('ns:%s' % FITTING_NODE_NAME,
1425                                namespaces={'ns': CANSAS_NS})
1426            if nodes:
1427                # Create an empty state
1428                state = PageState()
1429                state.from_xml(node=nodes[0])
1430
1431        except:
1432            logging.info("XML document does not contain fitting information.\n"
1433                         + traceback.format_exc())
1434
1435        return state
1436
1437    def _parse_simfit_state(self, entry):
1438        """
1439        Parses the saved data for a simultaneous fit
1440        :param entry: XML object to read from
1441        :return: XML object for a simultaneous fit or None
1442        """
1443        nodes = entry.xpath('ns:%s' % FITTING_NODE_NAME,
1444                            namespaces={'ns': CANSAS_NS})
1445        if nodes:
1446            simfitstate = nodes[0].xpath('ns:simultaneous_fit',
1447                                         namespaces={'ns': CANSAS_NS})
1448            if simfitstate:
1449                from simfitpage import SimFitPageState
1450                sim_fit_state = SimFitPageState()
1451                simfitstate_0 = simfitstate[0]
1452                all = simfitstate_0.xpath('ns:select_all',
1453                                          namespaces={'ns': CANSAS_NS})
1454                atts = all[0].attrib
1455                checked = atts.get('checked')
1456                sim_fit_state.select_all = bool(checked)
1457                model_list = simfitstate_0.xpath('ns:model_list',
1458                                                 namespaces={'ns': CANSAS_NS})
1459                model_list_items = model_list[0].xpath('ns:model_list_item',
1460                                                       namespaces={'ns':
1461                                                                    CANSAS_NS})
1462                for model in model_list_items:
1463                    attrs = model.attrib
1464                    sim_fit_state.model_list.append(attrs)
1465
1466                constraints = simfitstate_0.xpath('ns:constraints',
1467                                                namespaces={'ns': CANSAS_NS})
1468                constraint_list = constraints[0].xpath('ns:constraint',
1469                                               namespaces={'ns': CANSAS_NS})
1470                for constraint in constraint_list:
1471                    attrs = constraint.attrib
1472                    sim_fit_state.constraints_list.append(attrs)
1473
1474                return sim_fit_state
1475            else:
1476                return None
1477
1478    def _parse_save_state_entry(self, dom):
1479        """
1480        Parse a SASentry
1481
1482        :param node: SASentry node
1483
1484        :return: Data1D/Data2D object
1485
1486        """
1487        node = dom.xpath('ns:data_class', namespaces={'ns': CANSAS_NS})
1488        if not node or node[0].text.lstrip().rstrip() != "Data2D":
1489            return_value, _ = self._parse_entry(dom)
1490            numpy.trim_zeros(return_value.x)
1491            numpy.trim_zeros(return_value.y)
1492            numpy.trim_zeros(return_value.dy)
1493            size_dx = return_value.dx.size
1494            size_dxl = return_value.dxl.size
1495            size_dxw = return_value.dxw.size
1496            if size_dxl == 0 and size_dxw == 0:
1497                return_value.dxl = None
1498                return_value.dxw = None
1499                numpy.trim_zeros(return_value.dx)
1500            elif size_dx == 0:
1501                return_value.dx = None
1502                size_dx = size_dxl
1503                numpy.trim_zeros(return_value.dxl)
1504                numpy.trim_zeros(return_value.dxw)
1505
1506            return return_value, _
1507
1508        # Parse 2D
1509        data_info = Data2D()
1510
1511        # Look up title
1512        self._store_content('ns:Title', dom, 'title', data_info)
1513
1514        # Look up run number
1515        nodes = dom.xpath('ns:Run', namespaces={'ns': CANSAS_NS})
1516        for item in nodes:
1517            if item.text is not None:
1518                value = item.text.strip()
1519                if len(value) > 0:
1520                    data_info.run.append(value)
1521                    if item.get('name') is not None:
1522                        data_info.run_name[value] = item.get('name')
1523
1524        # Look up instrument name
1525        self._store_content('ns:SASinstrument/ns:name', dom,
1526                            'instrument', data_info)
1527
1528        # Notes
1529        note_list = dom.xpath('ns:SASnote', namespaces={'ns': CANSAS_NS})
1530        for note in note_list:
1531            try:
1532                if note.text is not None:
1533                    note_value = note.text.strip()
1534                    if len(note_value) > 0:
1535                        data_info.notes.append(note_value)
1536            except Exception:
1537                err_mess = "cansas_reader.read: error processing entry notes\n"
1538                err_mess += %s" % sys.exc_value
1539                self.errors.append(err_mess)
1540                logging.error(err_mess)
1541
1542        # Sample info ###################
1543        entry = get_content('ns:SASsample', dom)
1544        if entry is not None:
1545            data_info.sample.name = entry.get('name')
1546
1547        self._store_content('ns:SASsample/ns:ID', dom, 'ID', data_info.sample)
1548        self._store_float('ns:SASsample/ns:thickness', dom, 'thickness',
1549                          data_info.sample)
1550        self._store_float('ns:SASsample/ns:transmission', dom, 'transmission',
1551                          data_info.sample)
1552        self._store_float('ns:SASsample/ns:temperature', dom, 'temperature',
1553                          data_info.sample)
1554
1555        nodes = dom.xpath('ns:SASsample/ns:details',
1556                          namespaces={'ns': CANSAS_NS})
1557        for item in nodes:
1558            try:
1559                if item.text is not None:
1560                    detail_value = item.text.strip()
1561                    if len(detail_value) > 0:
1562                        data_info.sample.details.append(detail_value)
1563            except Exception:
1564                err_mess = "cansas_reader.read: error processing entry notes\n"
1565                err_mess += %s" % sys.exc_value
1566                self.errors.append(err_mess)
1567                logging.error(err_mess)
1568
1569        # Position (as a vector)
1570        self._store_float('ns:SASsample/ns:position/ns:x', dom, 'position.x',
1571                          data_info.sample)
1572        self._store_float('ns:SASsample/ns:position/ns:y', dom, 'position.y',
1573                          data_info.sample)
1574        self._store_float('ns:SASsample/ns:position/ns:z', dom, 'position.z',
1575                          data_info.sample)
1576
1577        # Orientation (as a vector)
1578        self._store_float('ns:SASsample/ns:orientation/ns:roll',
1579                          dom, 'orientation.x', data_info.sample)
1580        self._store_float('ns:SASsample/ns:orientation/ns:pitch',
1581                          dom, 'orientation.y', data_info.sample)
1582        self._store_float('ns:SASsample/ns:orientation/ns:yaw',
1583                          dom, 'orientation.z', data_info.sample)
1584
1585        # Source info ###################
1586        entry = get_content('ns:SASinstrument/ns:SASsource', dom)
1587        if entry is not None:
1588            data_info.source.name = entry.get('name')
1589
1590        self._store_content('ns:SASinstrument/ns:SASsource/ns:radiation',
1591                            dom, 'radiation', data_info.source)
1592        self._store_content('ns:SASinstrument/ns:SASsource/ns:beam_shape',
1593                            dom, 'beam_shape', data_info.source)
1594        self._store_float('ns:SASinstrument/ns:SASsource/ns:wavelength',
1595                          dom, 'wavelength', data_info.source)
1596        self._store_float('ns:SASinstrument/ns:SASsource/ns:wavelength_min',
1597                          dom, 'wavelength_min', data_info.source)
1598        self._store_float('ns:SASinstrument/ns:SASsource/ns:wavelength_max',
1599                          dom, 'wavelength_max', data_info.source)
1600        self._store_float('ns:SASinstrument/ns:SASsource/ns:wavelength_spread',
1601                          dom, 'wavelength_spread', data_info.source)
1602
1603        # Beam size (as a vector)
1604        entry = get_content('ns:SASinstrument/ns:SASsource/ns:beam_size', dom)
1605        if entry is not None:
1606            data_info.source.beam_size_name = entry.get('name')
1607
1608        self._store_float('ns:SASinstrument/ns:SASsource/ns:beam_size/ns:x',
1609                          dom, 'beam_size.x', data_info.source)
1610        self._store_float('ns:SASinstrument/ns:SASsource/ns:beam_size/ns:y',
1611                          dom, 'beam_size.y', data_info.source)
1612        self._store_float('ns:SASinstrument/ns:SASsource/ns:beam_size/ns:z',
1613                          dom, 'beam_size.z', data_info.source)
1614
1615        # Collimation info ###################
1616        nodes = dom.xpath('ns:SASinstrument/ns:SAScollimation',
1617                          namespaces={'ns': CANSAS_NS})
1618        for item in nodes:
1619            collim = Collimation()
1620            if item.get('name') is not None:
1621                collim.name = item.get('name')
1622            self._store_float('ns:length', item, 'length', collim)
1623
1624            # Look for apertures
1625            apert_list = item.xpath('ns:aperture',
1626                                    namespaces={'ns': CANSAS_NS})
1627            for apert in apert_list:
1628                aperture = Aperture()
1629
1630                # Get the name and type of the aperture
1631                aperture.name = apert.get('name')
1632                aperture.type = apert.get('type')
1633
1634                self._store_float('ns:distance', apert, 'distance', aperture)
1635
1636                entry = get_content('ns:size', apert)
1637                if entry is not None:
1638                    aperture.size_name = entry.get('name')
1639
1640                self._store_float('ns:size/ns:x', apert, 'size.x', aperture)
1641                self._store_float('ns:size/ns:y', apert, 'size.y', aperture)
1642                self._store_float('ns:size/ns:z', apert, 'size.z', aperture)
1643
1644                collim.aperture.append(aperture)
1645
1646            data_info.collimation.append(collim)
1647
1648        # Detector info ######################
1649        nodes = dom.xpath('ns:SASinstrument/ns:SASdetector',
1650                          namespaces={'ns': CANSAS_NS})
1651        for item in nodes:
1652
1653            detector = Detector()
1654
1655            self._store_content('ns:name', item, 'name', detector)
1656            self._store_float('ns:SDD', item, 'distance', detector)
1657
1658            # Detector offset (as a vector)
1659            self._store_float('ns:offset/ns:x', item, 'offset.x', detector)
1660            self._store_float('ns:offset/ns:y', item, 'offset.y', detector)
1661            self._store_float('ns:offset/ns:z', item, 'offset.z', detector)
1662
1663            # Detector orientation (as a vector)
1664            self._store_float('ns:orientation/ns:roll', item,
1665                              'orientation.x', detector)
1666            self._store_float('ns:orientation/ns:pitch', item,
1667                              'orientation.y', detector)
1668            self._store_float('ns:orientation/ns:yaw', item,
1669                              'orientation.z', detector)
1670
1671            # Beam center (as a vector)
1672            self._store_float('ns:beam_center/ns:x', item,
1673                              'beam_center.x', detector)
1674            self._store_float('ns:beam_center/ns:y', item,
1675                              'beam_center.y', detector)
1676            self._store_float('ns:beam_center/ns:z', item,
1677                              'beam_center.z', detector)
1678
1679            # Pixel size (as a vector)
1680            self._store_float('ns:pixel_size/ns:x', item,
1681                              'pixel_size.x', detector)
1682            self._store_float('ns:pixel_size/ns:y', item,
1683                              'pixel_size.y', detector)
1684            self._store_float('ns:pixel_size/ns:z', item,
1685                              'pixel_size.z', detector)
1686
1687            self._store_float('ns:slit_length', item, 'slit_length', detector)
1688
1689            data_info.detector.append(detector)
1690
1691        # Processes info ######################
1692        nodes = dom.xpath('ns:SASprocess', namespaces={'ns': CANSAS_NS})
1693        for item in nodes:
1694            process = Process()
1695            self._store_content('ns:name', item, 'name', process)
1696            self._store_content('ns:date', item, 'date', process)
1697            self._store_content('ns:description', item, 'description', process)
1698
1699            term_list = item.xpath('ns:term', namespaces={'ns': CANSAS_NS})
1700            for term in term_list:
1701                try:
1702                    term_attr = {}
1703                    for attr in term.keys():
1704                        term_attr[attr] = term.get(attr).strip()
1705                    if term.text is not None:
1706                        term_attr['value'] = term.text.strip()
1707                        process.term.append(term_attr)
1708                except:
1709                    err_mess = "cansas_reader.read: error processing "
1710                    err_mess += "entry notes\n  %s" % sys.exc_value
1711                    self.errors.append(err_mess)
1712                    logging.error(err_mess)
1713
1714            note_list = item.xpath('ns:SASprocessnote',
1715                                   namespaces={'ns': CANSAS_NS})
1716            for note in note_list:
1717                if note.text is not None:
1718                    process.notes.append(note.text.strip())
1719
1720            data_info.process.append(process)
1721
1722        # Data info ######################
1723        nodes = dom.xpath('ns:SASdata', namespaces={'ns': CANSAS_NS})
1724        if len(nodes) > 1:
1725            raise RuntimeError, "CanSAS reader is not compatible with" + \
1726                                " multiple SASdata entries"
1727
1728        for entry in nodes:
1729            for item in LIST_OF_DATA_2D_ATTR:
1730                # get node
1731                node = get_content('ns:%s' % item[0], entry)
1732                setattr(data_info, item[1], parse_entry_helper(node, item))
1733
1734            for item in LIST_OF_DATA_2D_VALUES:
1735                field = get_content('ns:%s' % item[0], entry)
1736                value_list = []
1737                if field is not None:
1738                    value_list = \
1739                        [parse_entry_helper(node, item) for node in field]
1740                if len(value_list) < 2:
1741                    setattr(data_info, item[0], None)
1742                else:
1743                    setattr(data_info, item[0], numpy.array(value_list))
1744
1745        return data_info
1746
1747    def _read_cansas(self, path):
1748        """
1749        Load data and fitting information from a CanSAS XML file.
1750
1751        :param path: file path
1752        :return: Data1D object if a single SASentry was found,
1753                    or a list of Data1D objects if multiple entries were found,
1754                    or None of nothing was found
1755        :raise RuntimeError: when the file can't be opened
1756        :raise ValueError: when the length of the data vectors are inconsistent
1757        """
1758        output = []
1759        simfitstate = None
1760        basename = os.path.basename(path)
1761        root, extension = os.path.splitext(basename)
1762        ext = extension.lower()
1763        try:
1764            if os.path.isfile(path):
1765                if ext in self.ext or ext == '.xml':
1766                    tree = etree.parse(path, parser=etree.ETCompatXMLParser())
1767                    # Check the format version number
1768                    # Specifying the namespace will take care of the file
1769                    # format version
1770                    root = tree.getroot()
1771                    entry_list = root.xpath('ns:SASentry',
1772                                            namespaces={'ns': CANSAS_NS})
1773                    for entry in entry_list:
1774                        try:
1775                            sas_entry, _ = self._parse_save_state_entry(entry)
1776                        except:
1777                            raise
1778                        fitstate = self._parse_state(entry)
1779
1780                        # state could be None when .svs file is loaded
1781                        # in this case, skip appending to output
1782                        if fitstate is not None:
1783                            sas_entry.meta_data['fitstate'] = fitstate
1784                            sas_entry.filename = fitstate.file
1785                            output.append(sas_entry)
1786
1787            else:
1788                self.call_back(format=ext)
1789                raise RuntimeError, "%s is not a file" % path
1790
1791            # Return output consistent with the loader's api
1792            if len(output) == 0:
1793                self.call_back(state=None, datainfo=None, format=ext)
1794                return None
1795            else:
1796                for ind in range(len(output)):
1797                    # Call back to post the new state
1798                    state = output[ind].meta_data['fitstate']
1799                    t = time.localtime(state.timestamp)
1800                    time_str = time.strftime("%b %d %H:%M", t)
1801                    # Check that no time stamp is already appended
1802                    max_char = state.file.find("[")
1803                    if max_char < 0:
1804                        max_char = len(state.file)
1805                    original_fname = state.file[0:max_char]
1806                    state.file = original_fname + ' [' + time_str + ']'
1807
1808                    if state is not None and state.is_data is not None:
1809                        output[ind].is_data = state.is_data
1810
1811                    output[ind].filename = state.file
1812                    state.data = output[ind]
1813                    state.data.name = output[ind].filename  # state.data_name
1814                    state.data.id = state.data_id
1815                    if state.is_data is not None:
1816                        state.data.is_data = state.is_data
1817                    if output[ind].run_name is not None\
1818                         and len(output[ind].run_name) != 0:
1819                        if isinstance(output[ind].run_name, dict):
1820                            name = output[ind].run_name.keys()[0]
1821                        else:
1822                            name = output[ind].run_name
1823                    else:
1824                        name = original_fname
1825                    state.data.group_id = name
1826                    # store state in fitting
1827                    self.call_back(state=state,
1828                                   datainfo=output[ind], format=ext)
1829                    self.state = state
1830                simfitstate = self._parse_simfit_state(entry)
1831                if simfitstate is not None:
1832                    self.call_back(state=simfitstate)
1833
1834                return output
1835        except:
1836            self.call_back(format=ext)
1837            raise
1838
1839    def write(self, filename, datainfo=None, fitstate=None):
1840        """
1841        Write the content of a Data1D as a CanSAS XML file only for standalone
1842
1843        :param filename: name of the file to write
1844        :param datainfo: Data1D object
1845        :param fitstate: PageState object
1846
1847        """
1848        # Sanity check
1849        if self.cansas:
1850            # Add fitting information to the XML document
1851            doc = self.write_toXML(datainfo, fitstate)
1852            # Write the XML document
1853        else:
1854            doc = fitstate.to_xml(file=filename)
1855
1856        # Save the document no matter the type
1857        fd = open(filename, 'w')
1858        fd.write(doc.toprettyxml())
1859        fd.close()
1860
1861    def write_toXML(self, datainfo=None, state=None, batchfit=None):
1862        """
1863        Write toXML, a helper for write(),
1864        could be used by guimanager._on_save()
1865
1866        : return: xml doc
1867        """
1868
1869        self.batchfit_params = batchfit
1870        if state.data is None or not state.data.is_data:
1871            return None
1872        # make sure title and data run are filled.
1873        if state.data.title is None or state.data.title == '':
1874            state.data.title = state.data.name
1875        if state.data.run_name is None or state.data.run_name == {}:
1876            state.data.run = [str(state.data.name)]
1877            state.data.run_name[0] = state.data.name
1878
1879        if issubclass(state.data.__class__,
1880                      sas.sascalc.dataloader.data_info.Data1D):
1881            data = state.data
1882            doc, sasentry = self._to_xml_doc(data)
1883        else:
1884            data = state.data
1885            doc, sasentry = self._data2d_to_xml_doc(data)
1886
1887        if state is not None:
1888            doc = state.to_xml(doc=doc, file=data.filename, entry_node=sasentry,
1889                               batch_fit_state=self.batchfit_params)
1890
1891        return doc
1892
1893# Simple html report templet
1894HEADER = "<html>\n"
1895HEADER += "<head>\n"
1896HEADER += "<meta http-equiv=Content-Type content='text/html; "
1897HEADER += "charset=windows-1252'> \n"
1898HEADER += "<meta name=Generator >\n"
1899HEADER += "</head>\n"
1900HEADER += "<body lang=EN-US>\n"
1901HEADER += "<div class=WordSection1>\n"
1902HEADER += "<p class=MsoNormal><b><span ><center><font size='4' >"
1903HEADER += "%s</font></center></span></center></b></p>"
1904HEADER += "<p class=MsoNormal>&nbsp;</p>"
1905PARA = "<p class=MsoNormal><font size='4' > %s \n"
1906PARA += "</font></p>"
1907CENTRE = "<p class=MsoNormal><center><font size='4' > %s \n"
1908CENTRE += "</font></center></p>"
1909FEET_1 = \
1910"""
1911<p class=MsoNormal>&nbsp;</p>
1912<br>
1913<p class=MsoNormal><b><span ><center> <font size='4' > Graph
1914</font></span></center></b></p>
1915<p class=MsoNormal>&nbsp;</p>
1916<center>
1917<br><font size='4' >Model Computation</font>
1918<br><font size='4' >Data: "%s"</font><br>
1919"""
1920FEET_2 = \
1921"""
1922<img src="%s" >
1923</img>
1924"""
1925FEET_3 = \
1926"""
1927</center>
1928</div>
1929</body>
1930</html>
1931"""
1932ELINE = "<p class=MsoNormal>&nbsp;</p>"
Note: See TracBrowser for help on using the repository browser.