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

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

Modified the cansas reader and the dataloader to be more explicit in the
errors that occurred when loading data files. Also, only each error now
only appears a single time.

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