source: sasview/src/sas/sasgui/perspectives/pr/inversion_state.py @ 7949dcf7

Last change on this file since 7949dcf7 was d85c194, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 9 years ago

Remaining modules refactored

  • Property mode set to 100644
File size: 18.8 KB
RevLine 
[1e62361]1"""
2    Handling of P(r) inversion states
3"""
[7116b6e0]4################################################################################
5#This software was developed by the University of Tennessee as part of the
6#Distributed Data Analysis of Neutron Scattering Experiments (DANSE)
[8b21fa7]7#project funded by the US National Science Foundation.
[7116b6e0]8#
9#See the license text in license.txt
10#
11#copyright 2009, University of Tennessee
12################################################################################
13
[6f1f129]14
[3e41f43]15import time
16import os
17import sys
[91128648]18import logging
[b1f7ec6]19from lxml import etree
[d85c194]20from sas.sasgui.guiframe.dataFitting import Data1D
[b699768]21from sas.sascalc.dataloader.readers.cansas_reader import Reader as CansasReader
22from sas.sascalc.dataloader.readers.cansas_reader import get_content
[91128648]23
24PRNODE_NAME = 'pr_inversion'
[b1f7ec6]25CANSAS_NS = "cansas1d/1.0"
[6f1f129]26
[80d2872]27# Translation of names between stored and object data
[8b21fa7]28## List of P(r) inversion inputs
29in_list = [["nterms", "nfunc"],
30           ["d_max", "d_max"],
31           ["alpha", "alpha"],
32           ["slit_width", "width"],
33           ["slit_height", "height"],
34           ["qmin", "qmin"],
35           ["qmax", "qmax"],
36           ["estimate_bck", "estimate_bck"]]
[6f1f129]37
38## List of P(r) inversion outputs
[7116b6e0]39out_list = [["elapsed", "elapsed"],
[8b21fa7]40            ["rg", "rg"],
41            ["iq0", "iq0"],
42            ["bck", "bck"],
43            ["chi2", "chi2"],
44            ["osc", "osc"],
45            ["pos", "pos"],
46            ["pos_err", "pos_err"],
47            ["alpha_estimate", "alpha_estimate"],
48            ["nterms_estimate", "nterms_estimate"]]
[6f1f129]49
50class InversionState(object):
51    """
[7116b6e0]52    Class to hold the state information of the InversionControl panel.
[6f1f129]53    """
54    def __init__(self):
55        """
[7116b6e0]56        Default values
[6f1f129]57        """
[8b21fa7]58        # Input
59        self.file = None
[6f1f129]60        self.estimate_bck = False
61        self.timestamp = time.time()
[8b21fa7]62
[6f1f129]63        # Inversion parameters
64        self.nfunc = None
65        self.d_max = None
66        self.alpha = None
[8b21fa7]67
[6f1f129]68        # Slit parameters
69        self.height = None
[8b21fa7]70        self.width = None
71
[6f1f129]72        # Q range
[8b21fa7]73        self.qmin = None
74        self.qmax = None
75
[6f1f129]76        # Outputs
77        self.elapsed = None
[8b21fa7]78        self.rg = None
79        self.iq0 = None
80        self.bck = None
81        self.chi2 = None
82        self.osc = None
83        self.pos = None
[6f1f129]84        self.pos_err = None
[8b21fa7]85
[6f1f129]86        # Estimates
87        self.alpha_estimate = None
88        self.nterms_estimate = None
[8b21fa7]89
[6f1f129]90        # Data
[8b21fa7]91        self.q = None
92        self.iq_obs = None
[6f1f129]93        self.iq_calc = None
[8b21fa7]94
[410aad8]95        # Coefficients
96        self.coefficients = None
97        self.covariance = None
[8b21fa7]98
[6f1f129]99    def __str__(self):
100        """
[7116b6e0]101        Pretty print
[8b21fa7]102
[7116b6e0]103        :return: string representing the state
[8b21fa7]104
[6f1f129]105        """
[8b21fa7]106        state = "File:         %s\n" % self.file
[6f1f129]107        state += "Timestamp:    %s\n" % self.timestamp
108        state += "Estimate bck: %s\n" % str(self.estimate_bck)
109        state += "No. terms:    %s\n" % str(self.nfunc)
110        state += "D_max:        %s\n" % str(self.d_max)
111        state += "Alpha:        %s\n" % str(self.alpha)
[8b21fa7]112
[6f1f129]113        state += "Slit height:  %s\n" % str(self.height)
114        state += "Slit width:   %s\n" % str(self.width)
[8b21fa7]115
[6f1f129]116        state += "Qmin:         %s\n" % str(self.qmin)
117        state += "Qmax:         %s\n" % str(self.qmax)
[8b21fa7]118
[6f1f129]119        state += "\nEstimates:\n"
120        state += "  Alpha:      %s\n" % str(self.alpha_estimate)
121        state += "  Nterms:     %s\n" % str(self.nterms_estimate)
[8b21fa7]122
[6f1f129]123        state += "\nOutputs:\n"
124        state += "  Elapsed:    %s\n" % str(self.elapsed)
125        state += "  Rg:         %s\n" % str(self.rg)
126        state += "  I(q=0):     %s\n" % str(self.iq0)
127        state += "  Bck:        %s\n" % str(self.bck)
128        state += "  Chi^2:      %s\n" % str(self.chi2)
129        state += "  Oscillation:%s\n" % str(self.osc)
130        state += "  Positive:   %s\n" % str(self.pos)
131        state += "  1-sigma pos:%s\n" % str(self.pos_err)
[8b21fa7]132
[6f1f129]133        return state
[8b21fa7]134
[91128648]135    def toXML(self, file="pr_state.prv", doc=None, entry_node=None):
[6f1f129]136        """
[7116b6e0]137        Writes the state of the InversionControl panel to file, as XML.
[8b21fa7]138
[7116b6e0]139        Compatible with standalone writing, or appending to an
140        already existing XML document. In that case, the XML document
[8b21fa7]141        is required. An optional entry node in the XML document
[3e41f43]142        may also be given.
[8b21fa7]143
[7116b6e0]144        :param file: file to write to
145        :param doc: XML document object [optional]
[8b21fa7]146        :param entry_node: XML node within the XML document at which
[7116b6e0]147            we will append the data [optional]
[8b21fa7]148
[6f1f129]149        """
[92a2ecd]150        #TODO: Get this to work
[6f1f129]151        from xml.dom.minidom import getDOMImplementation
152
[91128648]153        # Check whether we have to write a standalone XML file
154        if doc is None:
155            impl = getDOMImplementation()
[8b21fa7]156
157            doc_type = impl.createDocumentType(PRNODE_NAME, "1.0", "1.0")
158
[91128648]159            newdoc = impl.createDocument(None, PRNODE_NAME, doc_type)
160            top_element = newdoc.documentElement
161        else:
162            # We are appending to an existing document
163            newdoc = doc
164            top_element = newdoc.createElement(PRNODE_NAME)
165            if entry_node is None:
166                newdoc.documentElement.appendChild(top_element)
167            else:
168                entry_node.appendChild(top_element)
[8b21fa7]169
[6f1f129]170        attr = newdoc.createAttribute("version")
171        attr.nodeValue = '1.0'
172        top_element.setAttributeNode(attr)
[8b21fa7]173
[6f1f129]174        # File name
[410aad8]175        element = newdoc.createElement("filename")
[91128648]176        if self.file is not None:
177            element.appendChild(newdoc.createTextNode(str(self.file)))
[410aad8]178        else:
179            element.appendChild(newdoc.createTextNode(str(file)))
180        top_element.appendChild(element)
[8b21fa7]181
[6f1f129]182        element = newdoc.createElement("timestamp")
183        element.appendChild(newdoc.createTextNode(time.ctime(self.timestamp)))
184        attr = newdoc.createAttribute("epoch")
185        attr.nodeValue = str(self.timestamp)
186        element.setAttributeNode(attr)
187        top_element.appendChild(element)
[8b21fa7]188
[6f1f129]189        # Inputs
190        inputs = newdoc.createElement("inputs")
191        top_element.appendChild(inputs)
[8b21fa7]192
[6f1f129]193        for item in in_list:
194            element = newdoc.createElement(item[0])
[c4f6851]195            element.appendChild(newdoc.createTextNode(str(getattr(self, item[1]))))
[6f1f129]196            inputs.appendChild(element)
[8b21fa7]197
[6f1f129]198        # Outputs
199        outputs = newdoc.createElement("outputs")
200        top_element.appendChild(outputs)
[8b21fa7]201
[6f1f129]202        for item in out_list:
203            element = newdoc.createElement(item[0])
[c4f6851]204            element.appendChild(newdoc.createTextNode(str(getattr(self, item[1]))))
[6f1f129]205            outputs.appendChild(element)
[8b21fa7]206
[410aad8]207        # Save output coefficients and its covariance matrix
208        element = newdoc.createElement("coefficients")
209        element.appendChild(newdoc.createTextNode(str(self.coefficients)))
210        outputs.appendChild(element)
211        element = newdoc.createElement("covariance")
212        element.appendChild(newdoc.createTextNode(str(self.covariance)))
213        outputs.appendChild(element)
[8b21fa7]214
[6f1f129]215        # Save the file
[91128648]216        if doc is None:
217            fd = open(file, 'w')
218            fd.write(newdoc.toprettyxml())
219            fd.close()
220            return None
221        else:
[92a2ecd]222            return newdoc
[6f1f129]223
[91128648]224    def fromXML(self, file=None, node=None):
[6f1f129]225        """
[7116b6e0]226        Load a P(r) inversion state from a file
[8b21fa7]227
[7116b6e0]228        :param file: .prv file
229        :param node: node of a XML document to read from
[8b21fa7]230
[6f1f129]231        """
[91128648]232        if file is not None:
[3e41f43]233            msg = "InversionState no longer supports non-CanSAS"
234            msg += " format for P(r) files"
235            raise RuntimeError, msg
[8b21fa7]236
[3e41f43]237        if node.get('version') and node.get('version') == '1.0':
[8b21fa7]238
[b1f7ec6]239            # Get file name
240            entry = get_content('ns:filename', node)
241            if entry is not None:
242                self.file = entry.text.strip()
[8b21fa7]243
[b1f7ec6]244            # Get time stamp
245            entry = get_content('ns:timestamp', node)
246            if entry is not None and entry.get('epoch'):
247                try:
248                    self.timestamp = float(entry.get('epoch'))
249                except:
[3e41f43]250                    msg = "InversionState.fromXML: Could not read "
251                    msg += "timestamp\n %s" % sys.exc_value
252                    logging.error(msg)
[8b21fa7]253
[b1f7ec6]254            # Parse inversion inputs
255            entry = get_content('ns:inputs', node)
256            if entry is not None:
257                for item in in_list:
258                    input_field = get_content('ns:%s' % item[0], entry)
259                    if input_field is not None:
260                        try:
[c4f6851]261                            setattr(self, item[1], float(input_field.text.strip()))
[b1f7ec6]262                        except:
[c4f6851]263                            setattr(self, item[1], None)
[b1f7ec6]264                input_field = get_content('ns:estimate_bck', entry)
265                if input_field is not None:
266                    try:
[8b21fa7]267                        self.estimate_bck = input_field.text.strip() == 'True'
[b1f7ec6]268                    except:
269                        self.estimate_bck = False
[8b21fa7]270
[b1f7ec6]271            # Parse inversion outputs
272            entry = get_content('ns:outputs', node)
273            if entry is not None:
274                # Output parameters (scalars)
275                for item in out_list:
276                    input_field = get_content('ns:%s' % item[0], entry)
277                    if input_field is not None:
278                        try:
[c4f6851]279                            setattr(self, item[1], float(input_field.text.strip()))
[b1f7ec6]280                        except:
[c4f6851]281                            setattr(self, item[1], None)
[8b21fa7]282
[b1f7ec6]283                # Look for coefficients
284                # Format is [value, value, value, value]
285                coeff = get_content('ns:coefficients', entry)
286                if coeff is not None:
287                    # Remove brackets
[8b21fa7]288                    c_values = coeff.text.strip().replace('[', '')
289                    c_values = c_values.replace(']', '')
[b1f7ec6]290                    toks = c_values.split()
291                    self.coefficients = []
292                    for c in toks:
293                        try:
294                            self.coefficients.append(float(c))
295                        except:
296                            # Bad data, skip. We will count the number of
297                            # coefficients at the very end and deal with
298                            # inconsistencies then.
299                            pass
300                    # Sanity check
301                    if not len(self.coefficients) == self.nfunc:
[3e41f43]302                        # Inconsistent number of coefficients.
303                        # Don't keep the data.
304                        err_msg = "InversionState.fromXML: inconsistant "
305                        err_msg += "number of coefficients: "
306                        err_msg += "%d %d" % (len(self.coefficients),
307                                              self.nfunc)
[b1f7ec6]308                        logging.error(err_msg)
309                        self.coefficients = None
[8b21fa7]310
[b1f7ec6]311                # Look for covariance matrix
312                # Format is [ [value, value], [value, value] ]
313                coeff = get_content('ns:covariance', entry)
314                if coeff is not None:
315                    # Parse rows
316                    rows = coeff.text.strip().split('[')
317                    self.covariance = []
318                    for row in rows:
319                        row = row.strip()
320                        if len(row) == 0: continue
321                        # Remove end bracket
[8b21fa7]322                        row = row.replace(']', '')
[b1f7ec6]323                        c_values = row.split()
324                        cov_row = []
325                        for c in c_values:
326                            try:
327                                cov_row.append(float(c))
328                            except:
329                                # Bad data, skip. We will count the number of
330                                # coefficients at the very end and deal with
331                                # inconsistencies then.
332                                pass
333                        # Sanity check: check the number of entries in the row
334                        if len(cov_row) == self.nfunc:
335                            self.covariance.append(cov_row)
336                    # Sanity check: check the number of rows in the covariance
337                    # matrix
338                    if not len(self.covariance) == self.nfunc:
339                        # Inconsistent dimensions of the covariance matrix.
340                        # Don't keep the data.
[3e41f43]341                        err_msg = "InversionState.fromXML: "
342                        err_msg += "inconsistant dimensions of the "
343                        err_msg += " covariance matrix: "
[b1f7ec6]344                        err_msg += "%d %d" % (len(self.covariance), self.nfunc)
345                        logging.error(err_msg)
346                        self.covariance = None
[8b21fa7]347
[91128648]348class Reader(CansasReader):
[6f1f129]349    """
[7116b6e0]350    Class to load a .prv P(r) inversion file
[6f1f129]351    """
352    ## File type
353    type_name = "P(r)"
[8b21fa7]354
[6f1f129]355    ## Wildcards
[b35d3d1]356    type = ["P(r) files (*.prv)|*.prv",
[b9a5f0e]357            "SASView files (*.svs)|*.svs"]
[6f1f129]358    ## List of allowed extensions
[8b21fa7]359    ext = ['.prv', '.PRV', '.svs', '.SVS']
360
[0d88a09]361    def __init__(self, call_back, cansas=True):
[6f1f129]362        """
[7116b6e0]363        Initialize the call-back method to be called
364        after we load a file
[8b21fa7]365
[7116b6e0]366        :param call_back: call-back method
367        :param cansas:  True = files will be written/read in CanSAS format
368                        False = write CanSAS format
[8b21fa7]369
[6f1f129]370        """
[91128648]371        ## Call back method to be executed after a file is read
[6f1f129]372        self.call_back = call_back
[91128648]373        ## CanSAS format flag
374        self.cansas = cansas
[75fbd17]375        self.state = None
[8b21fa7]376
[6f1f129]377    def read(self, path):
[8b21fa7]378        """
[7116b6e0]379        Load a new P(r) inversion state from file
[8b21fa7]380
[7116b6e0]381        :param path: file path
[8b21fa7]382
[7116b6e0]383        :return: None
[8b21fa7]384
[6f1f129]385        """
[8b21fa7]386        if self.cansas == True:
[91128648]387            return self._read_cansas(path)
388        else:
389            return self._read_standalone(path)
[8b21fa7]390
[91128648]391    def _read_standalone(self, path):
[8b21fa7]392        """
[7116b6e0]393        Load a new P(r) inversion state from file.
394        The P(r) node is assumed to be the top element.
[8b21fa7]395
[7116b6e0]396        :param path: file path
[8b21fa7]397
[7116b6e0]398        :return: None
[8b21fa7]399
[91128648]400        """
[6f1f129]401        # Read the new state from file
402        state = InversionState()
[91128648]403        state.fromXML(file=path)
[8b21fa7]404
[6f1f129]405        # Call back to post the new state
[75fbd17]406        self.state = state
407        #self.call_back(state)
[6f1f129]408        return None
[8b21fa7]409
[91128648]410    def _parse_prstate(self, entry):
411        """
[7116b6e0]412        Read a p(r) inversion result from an XML node
[8b21fa7]413
414        :param entry: XML node to read from
415
[7116b6e0]416        :return: InversionState object
[8b21fa7]417
[91128648]418        """
[b35d3d1]419        state = None
[8b21fa7]420
[91128648]421        # Locate the P(r) node
422        try:
[3e41f43]423            nodes = entry.xpath('ns:%s' % PRNODE_NAME,
424                                namespaces={'ns': CANSAS_NS})
[8b21fa7]425            if nodes != []:
[b35d3d1]426                # Create an empty state
[8b21fa7]427                state = InversionState()
[b35d3d1]428                state.fromXML(node=nodes[0])
[91128648]429        except:
[3e41f43]430            msg = "XML document does not contain P(r) "
431            msg += "information.\n %s" % sys.exc_value
432            logging.info(msg)
[8b21fa7]433
[91128648]434        return state
[8b21fa7]435
[91128648]436    def _read_cansas(self, path):
[8b21fa7]437        """
[7116b6e0]438        Load data and P(r) information from a CanSAS XML file.
[8b21fa7]439
[7116b6e0]440        :param path: file path
[8b21fa7]441
442        :return: Data1D object if a single SASentry was found,
[7116b6e0]443                    or a list of Data1D objects if multiple entries were found,
444                    or None of nothing was found
[8b21fa7]445
[7116b6e0]446        :raise RuntimeError: when the file can't be opened
447        :raise ValueError: when the length of the data vectors are inconsistent
[8b21fa7]448
[91128648]449        """
450        output = []
[8b21fa7]451
[91128648]452        if os.path.isfile(path):
[8b21fa7]453            basename = os.path.basename(path)
[91128648]454            root, extension = os.path.splitext(basename)
[410aad8]455            #TODO: eventually remove the check for .xml once
456            # the P(r) writer/reader is truly complete.
[3e41f43]457            if  extension.lower() in self.ext or extension.lower() == '.xml':
[8b21fa7]458
[b1f7ec6]459                tree = etree.parse(path, parser=etree.ETCompatXMLParser())
[91128648]460                # Check the format version number
[3e41f43]461                # Specifying the namespace will take care of the file
462                #format version
[b1f7ec6]463                root = tree.getroot()
[8b21fa7]464
[3e41f43]465                entry_list = root.xpath('/ns:SASroot/ns:SASentry',
466                                        namespaces={'ns': CANSAS_NS})
[b1f7ec6]467
[91128648]468                for entry in entry_list:
[92a2ecd]469                    sas_entry, _ = self._parse_entry(entry)
[91128648]470                    prstate = self._parse_prstate(entry)
[b35d3d1]471                    #prstate could be None when .svs file is loaded
472                    #in this case, skip appending to output
473                    if prstate != None:
474                        sas_entry.meta_data['prstate'] = prstate
475                        sas_entry.filename = prstate.file
476                        output.append(sas_entry)
[91128648]477        else:
478            raise RuntimeError, "%s is not a file" % path
[8b21fa7]479
[91128648]480        # Return output consistent with the loader's api
[3e41f43]481        if len(output) == 0:
[91128648]482            return None
[3e41f43]483        elif len(output) == 1:
[410aad8]484            # Call back to post the new state
[8b21fa7]485            self.call_back(output[0].meta_data['prstate'], datainfo=output[0])
[229da98]486            #self.state = output[0].meta_data['prstate']
[91128648]487            return output[0]
488        else:
[8b21fa7]489            return output
490
491
[91128648]492    def write(self, filename, datainfo=None, prstate=None):
493        """
[7116b6e0]494        Write the content of a Data1D as a CanSAS XML file
[8b21fa7]495
[7116b6e0]496        :param filename: name of the file to write
497        :param datainfo: Data1D object
498        :param prstate: InversionState object
[8b21fa7]499
[91128648]500        """
501        # Sanity check
502        if self.cansas == True:
[8b21fa7]503            doc = self.write_toXML(datainfo, prstate)
[91128648]504            # Write the XML document
505            fd = open(filename, 'w')
506            fd.write(doc.toprettyxml())
507            fd.close()
508        else:
509            prstate.toXML(file=filename)
[8b21fa7]510
[b35d3d1]511    def write_toXML(self, datainfo=None, state=None):
512        """
513        Write toXML, a helper for write()
[8b21fa7]514
[b35d3d1]515        : return: xml doc
516        """
517        if datainfo is None:
[8b21fa7]518            datainfo = Data1D(x=[], y=[])
[a07e72f]519        elif not issubclass(datainfo.__class__, Data1D):
[3e41f43]520            msg = "The cansas writer expects a Data1D "
521            msg += "instance: %s" % str(datainfo.__class__.__name__)
522            raise RuntimeError, msg
[8b21fa7]523
[b35d3d1]524        # Create basic XML document
525        doc, sasentry = self._to_xml_doc(datainfo)
[8b21fa7]526
[b35d3d1]527        # Add the invariant information to the XML document
528        if state is not None:
[92a2ecd]529            doc = state.toXML(doc=doc, entry_node=sasentry)
[8b21fa7]530
531        return doc
Note: See TracBrowser for help on using the repository browser.