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

Last change on this file since d620d03c was 5251ec6, checked in by Paul Kienzle <pkienzle@…>, 6 years ago

improved support for py37 in sasgui

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