source: sasview/src/sas/dataloader/readers/cansas_reader.py @ b3efb7d

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.2release_4.0.1ticket-1009ticket-1094-headlessticket-1242-2d-resolutionticket-1243ticket-1249ticket885unittest-saveload
Last change on this file since b3efb7d was b3efb7d, checked in by krzywon, 9 years ago

Modified data loading error messages to be much more specific and
include information in the console log about the specific errors
generated.

The cansas reader is now loading non-standard units (ie counts), but
shows a specific error message about the issue.

  • Property mode set to 100644
File size: 52.2 KB
Line 
1"""
2    CanSAS data reader - new recursive cansas_version.
3"""
4############################################################################
5#This software was developed by the University of Tennessee as part of the
6#Distributed Data Analysis of Neutron Scattering Experiments (DANSE)
7#project funded by the US National Science Foundation.
8#If you use DANSE applications to do scientific research that leads to
9#publication, we ask that you acknowledge the use of the software with the
10#following sentence:
11#This work benefited from DANSE software developed under NSF award DMR-0520547.
12#copyright 2008,2009 University of Tennessee
13#############################################################################
14
15import logging
16import numpy
17import os
18import sys
19import datetime
20import inspect
21# For saving individual sections of data
22from sas.dataloader.data_info import Data1D
23from sas.dataloader.data_info import Collimation
24from sas.dataloader.data_info import TransmissionSpectrum
25from sas.dataloader.data_info import Detector
26from sas.dataloader.data_info import Process
27from sas.dataloader.data_info import Aperture
28# Both imports used. Do not remove either.
29from xml.dom.minidom import parseString
30import sas.dataloader.readers.xml_reader as xml_reader
31from sas.dataloader.readers.xml_reader import XMLreader
32from sas.dataloader.readers.cansas_constants import CansasConstants
33
34_ZERO = 1e-16
35PREPROCESS = "xmlpreprocess"
36ENCODING = "encoding"
37RUN_NAME_DEFAULT = "None"
38HAS_CONVERTER = True
39try:
40    from sas.data_util.nxsunit import Converter
41except ImportError:
42    HAS_CONVERTER = False
43
44CONSTANTS = CansasConstants()   
45CANSAS_FORMAT = CONSTANTS.format
46CANSAS_NS = CONSTANTS.names
47ALLOW_ALL = True
48
49
50# minidom used in functions called by outside classes
51import xml.dom.minidom
52# DO NOT REMOVE
53# Called by outside packages:
54#    sas.perspectives.invariant.invariant_state
55#    sas.perspectives.fitting.pagestate
56def get_content(location, node):
57    """
58    Get the first instance of the content of a xpath location.
59   
60    :param location: xpath location
61    :param node: node to start at
62   
63    :return: Element, or None
64    """
65    nodes = node.xpath(location, 
66                       namespaces={'ns': CANSAS_NS.get("1.0").get("ns")})
67   
68    if len(nodes) > 0:
69        return nodes[0]
70    else:
71        return None
72
73
74# DO NOT REMOVE
75# Called by outside packages:
76#    sas.perspectives.fitting.pagestate
77def write_node(doc, parent, name, value, attr=None):
78    """
79    :param doc: document DOM
80    :param parent: parent node
81    :param name: tag of the element
82    :param value: value of the child text node
83    :param attr: attribute dictionary
84   
85    :return: True if something was appended, otherwise False
86    """
87    if attr is None:
88        attr = {}
89    if value is not None:
90        node = doc.createElement(name)
91        node.appendChild(doc.createTextNode(str(value)))
92        for item in attr:
93            node.setAttribute(item, attr[item])
94        parent.appendChild(node)
95        return True
96    return False
97               
98
99class Reader(XMLreader):
100    """
101    Class to load cansas 1D XML files
102   
103    :Dependencies:
104        The CanSAS reader requires PyXML 0.8.4 or later.
105    """
106    ##CanSAS version - defaults to version 1.0
107    cansas_version = "1.0"
108    base_ns = "{cansas1d/1.0}"
109   
110    logging = []
111    errors = []
112   
113    type_name = "canSAS"
114    ## Wildcards
115    type = ["XML files (*.xml)|*.xml", "SasView Save Files (*.svs)|*.svs"]
116    ## List of allowed extensions
117    ext = ['.xml', '.XML', '.svs', '.SVS']
118   
119    ## Flag to bypass extension check
120    allow_all = True
121   
122   
123    def __init__(self):
124        ## List of errors
125        self.errors = []
126        self.encoding = None
127       
128       
129    def is_cansas(self, ext="xml"):
130        """
131        Checks to see if the xml file is a CanSAS file
132       
133        :param ext: The file extension of the data file
134        """
135        if self.validate_xml():
136            name = "{http://www.w3.org/2001/XMLSchema-instance}schemaLocation"
137            value = self.xmlroot.get(name)
138            if CANSAS_NS.get(self.cansas_version).get("ns") == \
139                    value.rsplit(" ")[0]:
140                return True
141        if ext == "svs":
142            return True
143        return False
144   
145   
146    def load_file_and_schema(self, xml_file):
147        """
148        Loads the file and associates a schema, if a known schema exists
149       
150        :param xml_file: The xml file path sent to Reader.read
151        """
152        base_name = xml_reader.__file__
153        base_name = base_name.replace("\\","/")
154        base = base_name.split("/sas/")[0]
155       
156        # Load in xml file and get the cansas version from the header
157        self.set_xml_file(xml_file)
158        self.cansas_version = self.xmlroot.get("version", "1.0")
159       
160        # Generic values for the cansas file based on the version
161        cansas_defaults = CANSAS_NS.get(self.cansas_version, "1.0")
162        schema_path = "{0}/sas/dataloader/readers/schema/{1}".format\
163                (base, cansas_defaults.get("schema")).replace("\\", "/")
164       
165        # Link a schema to the XML file.
166        self.set_schema(schema_path)
167        return cansas_defaults
168               
169   
170    def read(self, xml_file):
171        """
172        Validate and read in an xml_file file in the canSAS format.
173       
174        :param xml_file: A canSAS file path in proper XML format
175        """
176       
177        # output - Final list of Data1D objects
178        output = []
179        # ns - Namespace hierarchy for current xml_file object
180        ns_list = []
181       
182        # Check that the file exists
183        if os.path.isfile(xml_file):
184            basename = os.path.basename(xml_file)
185            _, extension = os.path.splitext(basename)
186            # If the file type is not allowed, return nothing
187            if extension in self.ext or self.allow_all:
188                # Get the file location of
189                cansas_defaults = self.load_file_and_schema(xml_file)
190               
191                # Try to load the file, but raise an error if unable to.
192                # Check the file matches the XML schema
193                try:
194                    if self.is_cansas(extension):
195                        # Get each SASentry from XML file and add it to a list.
196                        entry_list = self.xmlroot.xpath(
197                                '/ns:SASroot/ns:SASentry', 
198                                namespaces = {'ns': cansas_defaults.get("ns")})
199                        ns_list.append("SASentry")
200                       
201                        # If multiple files, modify the name for each is unique
202                        increment = 0
203                        # Parse each SASentry item
204                        for entry in entry_list:
205                            # Define a new Data1D object with zeroes for
206                            # x_vals and y_vals
207                            data1d = Data1D(numpy.empty(0), numpy.empty(0), \
208                                            numpy.empty(0), numpy.empty(0))
209                            data1d.dxl = numpy.empty(0)
210                            data1d.dxw = numpy.empty(0)
211                           
212                            # If more than one SASentry, increment each in order
213                            name = basename
214                            if len(entry_list) - 1 > 0:
215                                name += "_{0}".format(increment)
216                                increment += 1
217                           
218                            # Set the Data1D name and then parse the entry.
219                            # The entry is appended to a list of entry values
220                            data1d.filename = name
221                            data1d.meta_data["loader"] = "CanSAS 1D"
222                           
223                            # Get all preprocessing events and encoding
224                            self.set_processing_instructions()
225                            data1d.meta_data[PREPROCESS] = \
226                                    self.processing_instructions
227                           
228                            # Parse the XML file
229                            return_value, extras = \
230                                self._parse_entry(entry, ns_list, data1d)
231                            del extras[:]
232                           
233                            return_value = self._final_cleanup(return_value)
234                            output.append(return_value)
235                    else:
236                        output.append("Invalid XML at: {0}".format(\
237                                                    self.find_invalid_xml()))
238                except:
239                    # If the file does not match the schema, raise this error
240                    raise RuntimeError, "%s cannot be read" % xml_file
241                return output
242        # Return a list of parsed entries that dataloader can manage
243        return None
244   
245   
246    def _final_cleanup(self, data1d):
247        """
248        Final cleanup of the Data1D object to be sure it has all the
249        appropriate information needed for perspectives
250       
251        :param data1d: Data1D object that has been populated
252        """
253        # Final cleanup
254        # Remove empty nodes, verify array sizes are correct
255        for error in self.errors:
256            data1d.errors.append(error)
257        del self.errors[:]
258        numpy.trim_zeros(data1d.x)
259        numpy.trim_zeros(data1d.y)
260        numpy.trim_zeros(data1d.dy)
261        size_dx = data1d.dx.size
262        size_dxl = data1d.dxl.size
263        size_dxw = data1d.dxw.size
264        if size_dxl == 0 and size_dxw == 0:
265            data1d.dxl = None
266            data1d.dxw = None
267            numpy.trim_zeros(data1d.dx)
268        elif size_dx == 0:
269            data1d.dx = None
270            size_dx = size_dxl
271            numpy.trim_zeros(data1d.dxl)
272            numpy.trim_zeros(data1d.dxw)
273        return data1d
274                           
275   
276    def _create_unique_key(self, dictionary, name, numb = 0):
277        """
278        Create a unique key value for any dictionary to prevent overwriting
279        Recurses until a unique key value is found.
280       
281        :param dictionary: A dictionary with any number of entries
282        :param name: The index of the item to be added to dictionary
283        :param numb: The number to be appended to the name, starts at 0
284        """
285        if dictionary.get(name) is not None:
286            numb += 1
287            name = name.split("_")[0]
288            name += "_{0}".format(numb)
289            name = self._create_unique_key(dictionary, name, numb)
290        return name
291   
292   
293    def _unit_conversion(self, node, new_current_level, data1d, \
294                                                tagname, node_value):
295        """
296        A unit converter method used to convert the data included in the file
297        to the default units listed in data_info
298       
299        :param new_current_level: cansas_constants level as returned by
300            iterate_namespace
301        :param attr: The attributes of the node
302        :param data1d: Where the values will be saved
303        :param node_value: The value of the current dom node
304        """
305        attr = node.attrib
306        value_unit = ''
307        if 'unit' in attr and new_current_level.get('unit') is not None:
308            try:
309                local_unit = attr['unit']
310                if isinstance(node_value, float) is False:
311                    exec("node_value = float({0})".format(node_value))
312                default_unit = None
313                unitname = new_current_level.get("unit")
314                exec "default_unit = data1d.{0}".format(unitname)
315                if local_unit is not None and default_unit is not None and \
316                        local_unit.lower() != default_unit.lower() \
317                        and local_unit.lower() != "none":
318                    if HAS_CONVERTER == True:
319                        ## Check local units - bad units raise KeyError
320                        data_conv_q = Converter(local_unit)
321                        value_unit = default_unit
322                        i_string = "node_value = data_conv_q"
323                        i_string += "(node_value, units=data1d.{0})"
324                        exec i_string.format(unitname)
325                    else:
326                        value_unit = local_unit
327                        err_msg = "Unit converter is not available.\n"
328                        self.errors.append(err_msg)
329                else:
330                    value_unit = local_unit
331            except KeyError:
332                err_msg = "CanSAS reader: unexpected "
333                err_msg += "\"{0}\" unit [{1}]; "
334                err_msg = err_msg.format(tagname, local_unit)
335                intermediate = "err_msg += " + \
336                            "\"expecting [{1}]\"" + \
337                            ".format(data1d.{0})"
338                exec intermediate.format(unitname, "{0}", "{1}")
339                self.errors.append(err_msg)
340                value_unit = local_unit
341            except:
342                print sys.exc_info()
343                err_msg = "CanSAS reader: unknown error converting "
344                err_msg += "\"{0}\" unit [{1}]"
345                err_msg = err_msg.format(tagname, local_unit)
346                self.errors.append(err_msg)
347                value_unit = local_unit
348        elif 'unit' in attr:
349            value_unit = attr['unit']
350        node_value = "float({0})".format(node_value)
351        return node_value, value_unit
352   
353   
354    def _check_for_empty_data(self, data1d):
355        """
356        Creates an empty data set if no data is passed to the reader
357       
358        :param data1d: presumably a Data1D object
359        """
360        if data1d == None:
361            self.errors = []
362            x_vals = numpy.empty(0)
363            y_vals = numpy.empty(0)
364            dx_vals = numpy.empty(0)
365            dy_vals = numpy.empty(0)
366            dxl = numpy.empty(0)
367            dxw = numpy.empty(0)
368            data1d = Data1D(x_vals, y_vals, dx_vals, dy_vals)
369            data1d.dxl = dxl
370            data1d.dxw = dxw
371        return data1d
372   
373   
374    def _handle_special_cases(self, tagname, data1d, children):
375        """
376        Handle cases where the data type in Data1D is a dictionary or list
377       
378        :param tagname: XML tagname in use
379        :param data1d: The original Data1D object
380        :param children: Child nodes of node
381        :param node: existing node with tag name 'tagname'
382        """
383        if tagname == "SASdetector":
384            data1d = Detector()
385        elif tagname == "SAScollimation":
386            data1d = Collimation()
387        elif tagname == "SAStransmission_spectrum":
388            data1d = TransmissionSpectrum()
389        elif tagname == "SASprocess":
390            data1d = Process()
391            for child in children:
392                if child.tag.replace(self.base_ns, "") == "term":
393                    term_attr = {}
394                    for attr in child.keys():
395                        term_attr[attr] = \
396                            ' '.join(child.get(attr).split())
397                    if child.text is not None:
398                        term_attr['value'] = \
399                            ' '.join(child.text.split())
400                    data1d.term.append(term_attr)
401        elif tagname == "aperture":
402            data1d = Aperture()
403        if tagname == "Idata" and children is not None:
404            data1d = self._check_for_empty_resolution(data1d, children)
405        return data1d
406   
407   
408    def _check_for_empty_resolution(self, data1d, children):
409        """
410        A method to check all resolution data sets are the same size as I and Q
411        """
412        dql_exists = False
413        dqw_exists = False
414        dq_exists = False
415        di_exists = False
416        for child in children:
417            tag = child.tag.replace(self.base_ns, "")
418            if tag == "dQl":
419                dql_exists = True
420            if tag == "dQw":
421                dqw_exists = True
422            if tag == "Qdev":
423                dq_exists = True
424            if tag == "Idev":
425                di_exists = True
426        if dqw_exists and dql_exists == False:
427            data1d.dxl = numpy.append(data1d.dxl, 0.0)
428        elif dql_exists and dqw_exists == False:
429            data1d.dxw = numpy.append(data1d.dxw, 0.0)
430        elif dql_exists == False and dqw_exists == False \
431                                            and dq_exists == False:
432            data1d.dx = numpy.append(data1d.dx, 0.0)
433        if di_exists == False:
434            data1d.dy = numpy.append(data1d.dy, 0.0)
435        return data1d
436   
437   
438    def _restore_original_case(self, 
439                               tagname_original, 
440                               tagname, 
441                               save_data1d, 
442                               data1d):
443        """
444        Save the special case data to the appropriate location and restore
445        the original Data1D object
446       
447        :param tagname_original: Unmodified tagname for the node
448        :param tagname: modified tagname for the node
449        :param save_data1d: The original Data1D object
450        :param data1d: If a special case was handled, an object of that type
451        """
452        if tagname_original == "SASdetector":
453            save_data1d.detector.append(data1d)
454        elif tagname_original == "SAScollimation":
455            save_data1d.collimation.append(data1d)
456        elif tagname == "SAStransmission_spectrum":
457            save_data1d.trans_spectrum.append(data1d)
458        elif tagname_original == "SASprocess":
459            save_data1d.process.append(data1d)
460        elif tagname_original == "aperture":
461            save_data1d.aperture.append(data1d)
462        else:
463            save_data1d = data1d
464        return save_data1d
465   
466   
467    def _handle_attributes(self, node, data1d, cs_values, tagname):
468        """
469        Process all of the attributes for a node
470        """
471        attr = node.attrib
472        if attr is not None:
473            for key in node.keys():
474                try:
475                    node_value, unit = self._get_node_value(node, cs_values, \
476                                                        data1d, tagname)
477                    cansas_attrib = \
478                        cs_values.current_level.get("attributes").get(key)
479                    attrib_variable = cansas_attrib.get("variable")
480                    if key == 'unit' and unit != '':
481                        attrib_value = unit
482                    else:
483                        attrib_value = node.attrib[key]
484                    store_attr = attrib_variable.format("data1d", \
485                                                    attrib_value, key)
486                    exec store_attr
487                except AttributeError:
488                    pass   
489        return data1d
490   
491   
492    def _get_node_value(self, node, cs_values, data1d, tagname):
493        """
494        Get the value of a node and any applicable units
495       
496        :param node: The XML node to get the value of
497        :param cs_values: A CansasConstants.CurrentLevel object
498        :param attr: The node attributes
499        :param dataid: The working object to be modified
500        :param tagname: The tagname of the node
501        """
502        #Get the text from the node and convert all whitespace to spaces
503        units = ''
504        node_value = node.text
505        if node_value == "":
506            node_value = None
507        if node_value is not None:
508            node_value = ' '.join(node_value.split())
509       
510        # If the value is a float, compile with units.
511        if cs_values.ns_datatype == "float":
512            # If an empty value is given, set as zero.
513            if node_value is None or node_value.isspace() \
514                                    or node_value.lower() == "nan":
515                node_value = "0.0"
516            #Convert the value to the base units
517            node_value, units = self._unit_conversion(node, \
518                        cs_values.current_level, data1d, tagname, node_value)
519           
520        # If the value is a timestamp, convert to a datetime object
521        elif cs_values.ns_datatype == "timestamp":
522            if node_value is None or node_value.isspace():
523                pass
524            else:
525                try:
526                    node_value = \
527                        datetime.datetime.fromtimestamp(node_value)
528                except ValueError:
529                    node_value = None
530        return node_value, units
531   
532   
533    def _parse_entry(self, dom, names=None, data1d=None, extras=None):
534        """
535        Parse a SASEntry - new recursive method for parsing the dom of
536            the CanSAS data format. This will allow multiple data files
537            and extra nodes to be read in simultaneously.
538       
539        :param dom: dom object with a namespace base of names
540        :param names: A list of element names that lead up to the dom object
541        :param data1d: The data1d object that will be modified
542        :param extras: Any values that should go into meta_data when data1d
543            is not a Data1D object
544        """
545       
546        if extras is None:
547            extras = []
548        if names is None or names == []:
549            names = ["SASentry"]
550       
551        data1d = self._check_for_empty_data(data1d)
552                           
553        self.base_ns = "{0}{1}{2}".format("{", \
554                            CANSAS_NS.get(self.cansas_version).get("ns"), "}")
555        tagname = ''
556        tagname_original = ''
557       
558        # Go through each child in the parent element
559        for node in dom:
560            try:
561                # Get the element name and set the current names level
562                tagname = node.tag.replace(self.base_ns, "")
563                tagname_original = tagname
564                if tagname == "fitting_plug_in" or tagname == "pr_inversion" or\
565                    tagname == "invariant":
566                    continue
567                names.append(tagname)
568                children = node.getchildren()
569                if len(children) == 0:
570                    children = None
571                save_data1d = data1d
572               
573                # Look for special cases
574                data1d = self._handle_special_cases(tagname, data1d, children)
575               
576                # Get where to store content
577                cs_values = CONSTANTS.iterate_namespace(names)
578                # If the element is a child element, recurse
579                if children is not None:
580                    # Returned value is new Data1D object with all previous and
581                    # new values in it.
582                    data1d, extras = self._parse_entry(node, 
583                                                       names, data1d, extras)
584                   
585                #Get the information from the node
586                node_value, _ = self._get_node_value(node, cs_values, \
587                                                            data1d, tagname)
588               
589                # If appending to a dictionary (meta_data | run_name)
590                # make sure the key is unique
591                if cs_values.ns_variable == "{0}.meta_data[\"{2}\"] = \"{1}\"":
592                    # If we are within a Process, Detector, Collimation or
593                    # Aperture instance, pull out old data1d
594                    tagname = self._create_unique_key(data1d.meta_data, \
595                                                      tagname, 0)
596                    if isinstance(data1d, Data1D) == False:
597                        store_me = cs_values.ns_variable.format("data1d", \
598                                                            node_value, tagname)
599                        extras.append(store_me)
600                        cs_values.ns_variable = None
601                if cs_values.ns_variable == "{0}.run_name[\"{2}\"] = \"{1}\"":
602                    tagname = self._create_unique_key(data1d.run_name, \
603                                                      tagname, 0)
604               
605                # Check for Data1D object and any extra commands to save
606                if isinstance(data1d, Data1D):
607                    for item in extras:
608                        exec item
609                # Don't bother saving empty information unless it is a float
610                if cs_values.ns_variable is not None and \
611                            node_value is not None and \
612                            node_value.isspace() == False:
613                    # Format a string and then execute it.
614                    store_me = cs_values.ns_variable.format("data1d", \
615                                                            node_value, tagname)
616                    exec store_me
617                # Get attributes and process them
618                data1d = self._handle_attributes(node, data1d, cs_values, \
619                                                 tagname)
620               
621            except TypeError:
622                pass
623            except Exception as excep:
624                exc_type, exc_obj, exc_tb = sys.exc_info()
625                fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
626                print(excep, exc_type, fname, exc_tb.tb_lineno, \
627                      tagname, exc_obj)
628            finally:
629                # Save special cases in original data1d object
630                # then restore the data1d
631                save_data1d = self._restore_original_case(tagname_original, \
632                                                tagname, save_data1d, data1d)
633                if tagname_original == "fitting_plug_in" or \
634                    tagname_original == "invariant" or \
635                    tagname_original == "pr_inversion":
636                    pass
637                else:
638                    data1d = save_data1d
639                    # Remove tagname from names to restore original base
640                    names.remove(tagname_original)
641        return data1d, extras
642       
643   
644    def _get_pi_string(self):
645        """
646        Creates the processing instructions header for writing to file
647        """
648        pis = self.return_processing_instructions()
649        if len(pis) > 0:
650            pi_tree = self.create_tree(pis[0])
651            i = 1
652            for i in range(1, len(pis) - 1):
653                pi_tree = self.append(pis[i], pi_tree)
654            pi_string = self.to_string(pi_tree)
655        else:
656            pi_string = ""
657        return pi_string
658   
659   
660    def _create_main_node(self):
661        """
662        Creates the primary xml header used when writing to file
663        """
664        xsi = "http://www.w3.org/2001/XMLSchema-instance"
665        version = self.cansas_version
666        n_s = CANSAS_NS.get(version).get("ns")
667        if version == "1.1":
668            url = "http://www.cansas.org/formats/1.1/"
669        else:
670            url = "http://svn.smallangles.net/svn/canSAS/1dwg/trunk/"
671        schema_location = "{0} {1}cansas1d.xsd".format(n_s, url)
672        attrib = {"{" + xsi + "}schemaLocation" : schema_location,
673                  "version" : version}
674        nsmap = {'xsi' : xsi, None: n_s}
675       
676        main_node = self.create_element("{" + n_s + "}SASroot", \
677                                               attrib = attrib, \
678                                               nsmap = nsmap)
679        return main_node
680   
681   
682    def _write_run_names(self, datainfo, entry_node):
683        """
684        Writes the run names to the XML file
685       
686        :param datainfo: The Data1D object the information is coming from
687        :param entry_node: lxml node ElementTree object to be appended to
688        """
689        if datainfo.run == None or datainfo.run == []:
690            datainfo.run.append(RUN_NAME_DEFAULT)
691            datainfo.run_name[RUN_NAME_DEFAULT] = RUN_NAME_DEFAULT
692        for item in datainfo.run:
693            runname = {}
694            if item in datainfo.run_name and \
695            len(str(datainfo.run_name[item])) > 1:
696                runname = {'name': datainfo.run_name[item]}
697            self.write_node(entry_node, "Run", item, runname)
698           
699   
700    def _write_data(self, datainfo, entry_node):
701        """
702        Writes the I and Q data to the XML file
703       
704        :param datainfo: The Data1D object the information is coming from
705        :param entry_node: lxml node ElementTree object to be appended to
706        """
707        node = self.create_element("SASdata")
708        self.append(node, entry_node)
709       
710        for i in range(len(datainfo.x)):
711            point = self.create_element("Idata")
712            node.append(point)
713            self.write_node(point, "Q", datainfo.x[i], \
714                             {'unit': datainfo.x_unit})
715            if len(datainfo.y) >= i:
716                self.write_node(point, "I", datainfo.y[i],
717                            {'unit': datainfo.y_unit})
718            if datainfo.dy != None and len(datainfo.dy) > i:
719                self.write_node(point, "Idev", datainfo.dy[i],
720                            {'unit': datainfo.y_unit})
721            if datainfo.dx != None and len(datainfo.dx) > i:
722                self.write_node(point, "Qdev", datainfo.dx[i],
723                            {'unit': datainfo.x_unit})
724            if datainfo.dxw != None and len(datainfo.dxw) > i:
725                self.write_node(point, "dQw", datainfo.dxw[i],
726                            {'unit': datainfo.x_unit})
727            if datainfo.dxl != None and len(datainfo.dxl) > i:
728                self.write_node(point, "dQl", datainfo.dxl[i],
729                            {'unit': datainfo.x_unit})
730       
731   
732   
733    def _write_trans_spectrum(self, datainfo, entry_node):
734        """
735        Writes the transmission spectrum data to the XML file
736       
737        :param datainfo: The Data1D object the information is coming from
738        :param entry_node: lxml node ElementTree object to be appended to
739        """
740        for i in range(len(datainfo.trans_spectrum)):
741            spectrum = datainfo.trans_spectrum[i]
742            node = self.create_element("SAStransmission_spectrum",
743                                              {"name" : spectrum.name})
744            self.append(node, entry_node)
745            if isinstance(spectrum.timestamp, datetime.datetime):
746                node.setAttribute("timestamp", spectrum.timestamp)
747            for i in range(len(spectrum.wavelength)):
748                point = self.create_element("Tdata")
749                node.append(point)
750                self.write_node(point, "Lambda", spectrum.wavelength[i], 
751                           {'unit': spectrum.wavelength_unit})
752                self.write_node(point, "T", spectrum.transmission[i], 
753                           {'unit': spectrum.transmission_unit})
754                if spectrum.transmission_deviation != None \
755                and len(spectrum.transmission_deviation) >= i:
756                    self.write_node(point, "Tdev", \
757                               spectrum.transmission_deviation[i], \
758                               {'unit': spectrum.transmission_deviation_unit})
759   
760   
761    def _write_sample_info(self, datainfo, entry_node):
762        """
763        Writes the sample information to the XML file
764       
765        :param datainfo: The Data1D object the information is coming from
766        :param entry_node: lxml node ElementTree object to be appended to
767        """
768        sample = self.create_element("SASsample")
769        if datainfo.sample.name is not None:
770            self.write_attribute(sample, 
771                                        "name", 
772                                        str(datainfo.sample.name))
773        self.append(sample, entry_node)
774        self.write_node(sample, "ID", str(datainfo.sample.ID))
775        self.write_node(sample, "thickness", datainfo.sample.thickness,
776                   {"unit": datainfo.sample.thickness_unit})
777        self.write_node(sample, "transmission", datainfo.sample.transmission)
778        self.write_node(sample, "temperature", datainfo.sample.temperature,
779                   {"unit": datainfo.sample.temperature_unit})
780       
781        pos = self.create_element("position")
782        written = self.write_node(pos, 
783                                  "x", 
784                                  datainfo.sample.position.x,
785                                  {"unit": datainfo.sample.position_unit})
786        written = written | self.write_node(pos, 
787                                            "y", 
788                                            datainfo.sample.position.y,
789                                       {"unit": datainfo.sample.position_unit})
790        written = written | self.write_node(pos, 
791                                            "z",
792                                            datainfo.sample.position.z,
793                                       {"unit": datainfo.sample.position_unit})
794        if written == True:
795            self.append(pos, sample)
796       
797        ori = self.create_element("orientation")
798        written = self.write_node(ori, "roll",
799                                  datainfo.sample.orientation.x,
800                                  {"unit": datainfo.sample.orientation_unit})
801        written = written | self.write_node(ori, "pitch",
802                                       datainfo.sample.orientation.y,
803                                    {"unit": datainfo.sample.orientation_unit})
804        written = written | self.write_node(ori, "yaw",
805                                       datainfo.sample.orientation.z,
806                                    {"unit": datainfo.sample.orientation_unit})
807        if written == True:
808            self.append(ori, sample)
809       
810        for item in datainfo.sample.details:
811            self.write_node(sample, "details", item)
812   
813   
814    def _write_instrument(self, datainfo, entry_node):
815        """
816        Writes the instrumental information to the XML file
817       
818        :param datainfo: The Data1D object the information is coming from
819        :param entry_node: lxml node ElementTree object to be appended to
820        """
821        instr = self.create_element("SASinstrument")
822        self.append(instr, entry_node)
823        self.write_node(instr, "name", datainfo.instrument)
824        return instr
825   
826   
827    def _write_source(self, datainfo, instr):
828        """
829        Writes the source information to the XML file
830       
831        :param datainfo: The Data1D object the information is coming from
832        :param instr: instrument node  to be appended to
833        """
834        source = self.create_element("SASsource")
835        if datainfo.source.name is not None:
836            self.write_attribute(source,
837                                        "name",
838                                        str(datainfo.source.name))
839        self.append(source, instr)
840        if datainfo.source.radiation == None or datainfo.source.radiation == '':
841            datainfo.source.radiation = "neutron"
842        self.write_node(source, "radiation", datainfo.source.radiation)
843       
844        size = self.create_element("beam_size")
845        if datainfo.source.beam_size_name is not None:
846            self.write_attribute(size,
847                                        "name",
848                                        str(datainfo.source.beam_size_name))
849        written = self.write_node(size, "x", datainfo.source.beam_size.x,
850                             {"unit": datainfo.source.beam_size_unit})
851        written = written | self.write_node(size, "y",
852                                       datainfo.source.beam_size.y,
853                                       {"unit": datainfo.source.beam_size_unit})
854        written = written | self.write_node(size, "z",
855                                       datainfo.source.beam_size.z,
856                                       {"unit": datainfo.source.beam_size_unit})
857        if written == True:
858            self.append(size, source)
859           
860        self.write_node(source, "beam_shape", datainfo.source.beam_shape)
861        self.write_node(source, "wavelength",
862                   datainfo.source.wavelength,
863                   {"unit": datainfo.source.wavelength_unit})
864        self.write_node(source, "wavelength_min",
865                   datainfo.source.wavelength_min,
866                   {"unit": datainfo.source.wavelength_min_unit})
867        self.write_node(source, "wavelength_max",
868                   datainfo.source.wavelength_max,
869                   {"unit": datainfo.source.wavelength_max_unit})
870        self.write_node(source, "wavelength_spread",
871                   datainfo.source.wavelength_spread,
872                   {"unit": datainfo.source.wavelength_spread_unit})
873   
874   
875    def _write_collimation(self, datainfo, instr):   
876        """
877        Writes the collimation information to the XML file
878       
879        :param datainfo: The Data1D object the information is coming from
880        :param instr: lxml node ElementTree object to be appended to
881        """
882        if datainfo.collimation == [] or datainfo.collimation == None:
883            coll = Collimation()
884            datainfo.collimation.append(coll)
885        for item in datainfo.collimation:
886            coll = self.create_element("SAScollimation")
887            if item.name is not None:
888                self.write_attribute(coll, "name", str(item.name))
889            self.append(coll, instr)
890           
891            self.write_node(coll, "length", item.length,
892                       {"unit": item.length_unit})
893           
894            for aperture in item.aperture:
895                apert = self.create_element("aperture")
896                if aperture.name is not None:
897                    self.write_attribute(apert, "name", str(aperture.name))
898                if aperture.type is not None:
899                    self.write_attribute(apert, "type", str(aperture.type))
900                self.append(apert, coll)
901               
902                size = self.create_element("size")
903                if aperture.size_name is not None:
904                    self.write_attribute(size, 
905                                                "name", 
906                                                str(aperture.size_name))
907                written = self.write_node(size, "x", aperture.size.x,
908                                     {"unit": aperture.size_unit})
909                written = written | self.write_node(size, "y", aperture.size.y,
910                                               {"unit": aperture.size_unit})
911                written = written | self.write_node(size, "z", aperture.size.z,
912                                               {"unit": aperture.size_unit})
913                if written == True:
914                    self.append(size, apert)
915               
916                self.write_node(apert, "distance", aperture.distance,
917                           {"unit": aperture.distance_unit})
918   
919   
920    def _write_detectors(self, datainfo, instr):
921        """
922        Writes the detector information to the XML file
923       
924        :param datainfo: The Data1D object the information is coming from
925        :param inst: lxml instrument node to be appended to
926        """
927        if datainfo.detector == None or datainfo.detector == []:
928            det = Detector()
929            det.name = ""
930            datainfo.detector.append(det)
931               
932        for item in datainfo.detector:
933            det = self.create_element("SASdetector")
934            written = self.write_node(det, "name", item.name)
935            written = written | self.write_node(det, "SDD", item.distance,
936                                           {"unit": item.distance_unit})
937            if written == True:
938                self.append(det, instr)
939           
940            off = self.create_element("offset")
941            written = self.write_node(off, "x", item.offset.x,
942                                 {"unit": item.offset_unit})
943            written = written | self.write_node(off, "y", item.offset.y,
944                                           {"unit": item.offset_unit})
945            written = written | self.write_node(off, "z", item.offset.z,
946                                           {"unit": item.offset_unit})
947            if written == True:
948                self.append(off, det)
949               
950            ori = self.create_element("orientation")
951            written = self.write_node(ori, "roll", item.orientation.x,
952                                 {"unit": item.orientation_unit})
953            written = written | self.write_node(ori, "pitch",
954                                           item.orientation.y,
955                                           {"unit": item.orientation_unit})
956            written = written | self.write_node(ori, "yaw",
957                                           item.orientation.z,
958                                           {"unit": item.orientation_unit})
959            if written == True:
960                self.append(ori, det)
961           
962            center = self.create_element("beam_center")
963            written = self.write_node(center, "x", item.beam_center.x,
964                                 {"unit": item.beam_center_unit})
965            written = written | self.write_node(center, "y",
966                                           item.beam_center.y,
967                                           {"unit": item.beam_center_unit})
968            written = written | self.write_node(center, "z",
969                                           item.beam_center.z,
970                                           {"unit": item.beam_center_unit})
971            if written == True:
972                self.append(center, det)
973               
974            pix = self.create_element("pixel_size")
975            written = self.write_node(pix, "x", item.pixel_size.x,
976                                 {"unit": item.pixel_size_unit})
977            written = written | self.write_node(pix, "y", item.pixel_size.y,
978                                           {"unit": item.pixel_size_unit})
979            written = written | self.write_node(pix, "z", item.pixel_size.z,
980                                           {"unit": item.pixel_size_unit})
981            written = written | self.write_node(det, "slit_length",
982                                           item.slit_length,
983                                           {"unit": item.slit_length_unit})
984            if written == True:
985                self.append(pix, det)
986   
987   
988    def _write_process_notes(self, datainfo, entry_node):
989        """
990        Writes the process notes to the XML file
991       
992        :param datainfo: The Data1D object the information is coming from
993        :param entry_node: lxml node ElementTree object to be appended to
994       
995        """
996        for item in datainfo.process:
997            node = self.create_element("SASprocess")
998            self.append(node, entry_node)
999
1000            self.write_node(node, "name", item.name)
1001            self.write_node(node, "date", item.date)
1002            self.write_node(node, "description", item.description)
1003            for term in item.term:
1004                value = term['value']
1005                del term['value']
1006                self.write_node(node, "term", value, term)
1007            for note in item.notes:
1008                self.write_node(node, "SASprocessnote", note)
1009            if len(item.notes) == 0:
1010                self.write_node(node, "SASprocessnote", "")
1011   
1012   
1013    def _write_notes(self, datainfo, entry_node):
1014        """
1015        Writes the notes to the XML file and creates an empty note if none
1016        exist
1017       
1018        :param datainfo: The Data1D object the information is coming from
1019        :param entry_node: lxml node ElementTree object to be appended to
1020       
1021        """
1022        if len(datainfo.notes) == 0:
1023            node = self.create_element("SASnote")
1024            self.append(node, entry_node)
1025        else:
1026            for item in datainfo.notes:
1027                node = self.create_element("SASnote")
1028                self.write_text(node, item)
1029                self.append(node, entry_node)
1030   
1031   
1032    def _check_origin(self, entry_node, doc):
1033        """
1034        Return the document, and the SASentry node associated with
1035        the data we just wrote.
1036        If the calling function was not the cansas reader, return a minidom
1037        object rather than an lxml object.
1038       
1039        :param entry_node: lxml node ElementTree object to be appended to
1040        :param doc: entire xml tree
1041        """
1042        frm = inspect.stack()[1]
1043        mod_name = frm[1].replace("\\", "/").replace(".pyc", "")
1044        mod_name = mod_name.replace(".py", "")
1045        mod = mod_name.split("sas/")
1046        mod_name = mod[1]
1047        if mod_name != "dataloader/readers/cansas_reader":
1048            string = self.to_string(doc, pretty_print=False)
1049            doc = parseString(string)
1050            node_name = entry_node.tag
1051            node_list = doc.getElementsByTagName(node_name)
1052            entry_node = node_list.item(0)
1053        return entry_node
1054   
1055   
1056    def _to_xml_doc(self, datainfo):
1057        """
1058        Create an XML document to contain the content of a Data1D
1059       
1060        :param datainfo: Data1D object
1061        """
1062        if not issubclass(datainfo.__class__, Data1D):
1063            raise RuntimeError, "The cansas writer expects a Data1D instance"
1064       
1065        # Get PIs and create root element
1066        pi_string = self._get_pi_string()
1067       
1068        # Define namespaces and create SASroot object
1069        main_node = self._create_main_node()
1070       
1071        # Create ElementTree, append SASroot and apply processing instructions
1072        base_string = pi_string + self.to_string(main_node)
1073        base_element = self.create_element_from_string(base_string)
1074        doc = self.create_tree(base_element)
1075       
1076        # Create SASentry Element
1077        entry_node = self.create_element("SASentry")
1078        root = doc.getroot()
1079        root.append(entry_node)
1080       
1081        # Add Title to SASentry
1082        self.write_node(entry_node, "Title", datainfo.title)
1083       
1084        # Add Run to SASentry
1085        self._write_run_names(datainfo, entry_node)
1086       
1087        # Add Data info to SASEntry
1088        self._write_data(datainfo, entry_node)
1089       
1090        # Transmission Spectrum Info
1091        self._write_trans_spectrum(datainfo, entry_node)
1092       
1093        # Sample info
1094        self._write_sample_info(datainfo, entry_node)
1095       
1096        # Instrument info
1097        instr = self._write_instrument(datainfo, entry_node)
1098       
1099        #   Source
1100        self._write_source(datainfo, instr)
1101       
1102        #   Collimation
1103        self._write_collimation(datainfo, instr)
1104
1105        #   Detectors
1106        self._write_detectors(datainfo, instr)
1107           
1108        # Processes info
1109        self._write_process_notes(datainfo, entry_node)
1110               
1111        # Note info
1112        self._write_notes(datainfo, entry_node) 
1113       
1114        # Return the document, and the SASentry node associated with
1115        #      the data we just wrote
1116        # If the calling function was not the cansas reader, return a minidom
1117        #      object rather than an lxml object.       
1118        entry_node = self._check_origin(entry_node, doc)
1119       
1120        return doc, entry_node
1121   
1122   
1123    def write_node(self, parent, name, value, attr=None):
1124        """
1125        :param doc: document DOM
1126        :param parent: parent node
1127        :param name: tag of the element
1128        :param value: value of the child text node
1129        :param attr: attribute dictionary
1130       
1131        :return: True if something was appended, otherwise False
1132        """
1133        if value is not None:
1134            parent = self.ebuilder(parent, name, value, attr)
1135            return True
1136        return False
1137   
1138           
1139    def write(self, filename, datainfo):
1140        """
1141        Write the content of a Data1D as a CanSAS XML file
1142       
1143        :param filename: name of the file to write
1144        :param datainfo: Data1D object
1145        """
1146        # Create XML document
1147        doc, _ = self._to_xml_doc(datainfo)
1148        # Write the file
1149        file_ref = open(filename, 'w')
1150        if self.encoding == None:
1151            self.encoding = "UTF-8"
1152        doc.write(file_ref, encoding=self.encoding,
1153                  pretty_print=True, xml_declaration=True)
1154        file_ref.close()
1155   
1156   
1157    # DO NOT REMOVE - used in saving and loading panel states.
1158    def _store_float(self, location, node, variable, storage, optional=True):
1159        """
1160        Get the content of a xpath location and store
1161        the result. Check that the units are compatible
1162        with the destination. The value is expected to
1163        be a float.
1164       
1165        The xpath location might or might not exist.
1166        If it does not exist, nothing is done
1167       
1168        :param location: xpath location to fetch
1169        :param node: node to read the data from
1170        :param variable: name of the data member to store it in [string]
1171        :param storage: data object that has the 'variable' data member
1172        :param optional: if True, no exception will be raised
1173            if unit conversion can't be done
1174
1175        :raise ValueError: raised when the units are not recognized
1176        """
1177        entry = get_content(location, node)
1178        try:
1179            value = float(entry.text)
1180        except:
1181            value = None
1182           
1183        if value is not None:
1184            # If the entry has units, check to see that they are
1185            # compatible with what we currently have in the data object
1186            units = entry.get('unit')
1187            if units is not None:
1188                toks = variable.split('.')
1189                local_unit = None
1190                exec "local_unit = storage.%s_unit" % toks[0]
1191                if local_unit != None and units.lower() != local_unit.lower():
1192                    if HAS_CONVERTER == True:
1193                        try:
1194                            conv = Converter(units)
1195                            exec "storage.%s = %g" % (variable,
1196                                           conv(value, units=local_unit))
1197                        except:
1198                            _, exc_value, _ = sys.exc_info()
1199                            err_mess = "CanSAS reader: could not convert"
1200                            err_mess += " %s unit [%s]; expecting [%s]\n  %s" \
1201                                % (variable, units, local_unit, exc_value)
1202                            self.errors.append(err_mess)
1203                            if optional:
1204                                logging.info(err_mess)
1205                            else:
1206                                raise ValueError, err_mess
1207                    else:
1208                        err_mess = "CanSAS reader: unrecognized %s unit [%s];"\
1209                        % (variable, units)
1210                        err_mess += " expecting [%s]" % local_unit
1211                        self.errors.append(err_mess)
1212                        if optional:
1213                            logging.info(err_mess)
1214                        else:
1215                            raise ValueError, err_mess
1216                else:
1217                    exec "storage.%s = value" % variable
1218            else:
1219                exec "storage.%s = value" % variable
1220               
1221   
1222    # DO NOT REMOVE - used in saving and loading panel states.
1223    def _store_content(self, location, node, variable, storage):
1224        """
1225        Get the content of a xpath location and store
1226        the result. The value is treated as a string.
1227       
1228        The xpath location might or might not exist.
1229        If it does not exist, nothing is done
1230       
1231        :param location: xpath location to fetch
1232        :param node: node to read the data from
1233        :param variable: name of the data member to store it in [string]
1234        :param storage: data object that has the 'variable' data member
1235       
1236        :return: return a list of errors
1237        """
1238        entry = get_content(location, node)
1239        if entry is not None and entry.text is not None:
1240            exec "storage.%s = entry.text.strip()" % variable
Note: See TracBrowser for help on using the repository browser.