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

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 a321c52 was a321c52, checked in by krzywon, 7 years ago

Force all models to go through _convert_to_sasmodels routine to ensure all sasmodels version changes are applied.

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