source: sasview/src/sas/sascalc/realspace/VolumeCanvas.py @ 7677b4d

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

Remaining modules refactored

  • Property mode set to 100644
File size: 27.6 KB
Line 
1#!/usr/bin/env python
2""" Volume Canvas
3
4    Simulation canvas for real-space simulation of SAS scattering intensity.
5    The user can create an arrangement of basic shapes and estimate I(q) and
6    I(q_x, q_y). Error estimates on the simulation are also available.
7   
8    Example:
9   
10    import sas.sascalc.realspace.VolumeCanvas as VolumeCanvas
11    canvas = VolumeCanvas.VolumeCanvas()
12    canvas.setParam('lores_density', 0.01)
13   
14    sphere = SphereDescriptor()
15    handle = canvas.addObject(sphere)
16
17    output, error = canvas.getIqError(q=0.1)
18    output, error = canvas.getIq2DError(0.1, 0.1)
19   
20    or alternatively:
21    iq = canvas.run(0.1)
22    i2_2D = canvas.run([0.1, 1.57])
23   
24"""
25
26from sas.models.BaseComponent import BaseComponent
27from sas.sascalc.simulation.pointsmodelpy import pointsmodelpy
28from sas.sascalc.simulation.geoshapespy import geoshapespy
29
30
31import os.path, math
32
33class ShapeDescriptor:
34    """
35        Class to hold the information about a shape
36        The descriptor holds a dictionary of parameters.
37       
38        Note: if shape parameters are accessed directly
39        from outside VolumeCanvas. The getPr method
40        should be called before evaluating I(q).
41               
42    """
43    def __init__(self):
44        """
45            Initialization
46        """
47        ## Real space object
48        self.shapeObject = None
49        ## Parameters of the object
50        self.params = {}
51        self.params["center"] = [0, 0, 0]
52        # Orientation are angular offsets in degrees with respect to X, Y, Z
53        self.params["orientation"] = [0, 0, 0]
54        # Default to lores shape
55        self.params['is_lores'] = True
56        self.params['order'] = 0
57           
58    def create(self):
59        """
60            Create an instance of the shape
61        """
62        # Set center
63        x0 = self.params["center"][0]
64        y0 = self.params["center"][1]
65        z0 = self.params["center"][2]
66        geoshapespy.set_center(self.shapeObject, x0, y0, z0)
67       
68        # Set orientation
69        x0 = self.params["orientation"][0]
70        y0 = self.params["orientation"][1]
71        z0 = self.params["orientation"][2]
72        geoshapespy.set_orientation(self.shapeObject, x0, y0, z0)
73               
74class SphereDescriptor(ShapeDescriptor):
75    """
76        Descriptor for a sphere
77       
78        The parameters are:
79            - radius [Angstroem] [default = 20 A]
80            - Contrast [A-2] [default = 1 A-2]
81           
82    """
83    def __init__(self):
84        """
85            Initialization
86        """ 
87        ShapeDescriptor.__init__(self)
88        # Default parameters
89        self.params["type"]   = "sphere"
90        # Radius of the sphere
91        self.params["radius"] = 20.0
92        # Constrast parameter
93        self.params["contrast"] = 1.0
94
95    def create(self):
96        """
97            Create an instance of the shape
98            @return: instance of the shape
99        """
100        self.shapeObject = geoshapespy.new_sphere(\
101            self.params["radius"])
102       
103        ShapeDescriptor.create(self)   
104        return self.shapeObject
105   
106class CylinderDescriptor(ShapeDescriptor):
107    """
108        Descriptor for a cylinder
109        Orientation: Default cylinder is along Y
110       
111        Parameters:
112            - Length [default = 40 A]
113            - Radius [default = 10 A]
114            - Contrast [default = 1 A-2]
115    """
116    def __init__(self):
117        """
118            Initialization
119        """ 
120        ShapeDescriptor.__init__(self)
121        # Default parameters
122        self.params["type"]   = "cylinder"
123        # Length of the cylinder
124        self.params["length"] = 40.0
125        # Radius of the cylinder
126        self.params["radius"] = 10.0
127        # Constrast parameter
128        self.params["contrast"] = 1.0
129       
130    def create(self):
131        """
132            Create an instance of the shape
133            @return: instance of the shape
134        """
135        self.shapeObject = geoshapespy.new_cylinder(\
136            self.params["radius"], self.params["length"])
137
138        ShapeDescriptor.create(self)
139        return self.shapeObject
140       
141
142class EllipsoidDescriptor(ShapeDescriptor):
143    """
144        Descriptor for an ellipsoid
145       
146        Parameters:
147            - Radius_x along the x-axis [default = 30 A]
148            - Radius_y along the y-axis [default = 20 A]
149            - Radius_z along the z-axis [default = 10 A]
150            - contrast [default = 1 A-2]
151    """
152    def __init__(self):
153        """
154            Initialization
155        """ 
156        ShapeDescriptor.__init__(self)
157        # Default parameters
158        self.params["type"]   = "ellipsoid"
159        self.params["radius_x"] = 30.0
160        self.params["radius_y"] = 20.0
161        self.params["radius_z"] = 10.0
162        self.params["contrast"] = 1.0
163       
164    def create(self):
165        """
166            Create an instance of the shape
167            @return: instance of the shape
168        """
169        self.shapeObject = geoshapespy.new_ellipsoid(\
170            self.params["radius_x"], self.params["radius_y"], 
171            self.params["radius_z"])
172       
173        ShapeDescriptor.create(self)   
174        return self.shapeObject
175       
176class HelixDescriptor(ShapeDescriptor):
177    """
178        Descriptor for an helix
179       
180        Parameters:
181            -radius_helix: the radius of the helix [default = 10 A]
182            -radius_tube: radius of the "tube" that forms the helix [default = 3 A]
183            -pitch: distance between two consecutive turns of the helix [default = 34 A]
184            -turns: number of turns of the helix [default = 3]
185            -contrast: contrast parameter [default = 1 A-2]
186    """
187    def __init__(self):
188        """
189            Initialization
190        """ 
191        ShapeDescriptor.__init__(self)
192        # Default parameters
193        self.params["type"]   = "singlehelix"
194        self.params["radius_helix"] = 10.0
195        self.params["radius_tube"] = 3.0
196        self.params["pitch"] = 34.0
197        self.params["turns"] = 3.0
198        self.params["contrast"] = 1.0
199
200    def create(self):
201        """
202            Create an instance of the shape
203            @return: instance of the shape
204        """
205        self.shapeObject = geoshapespy.new_singlehelix(\
206            self.params["radius_helix"], self.params["radius_tube"], 
207            self.params["pitch"], self.params["turns"])
208       
209        ShapeDescriptor.create(self)   
210        return self.shapeObject
211       
212class PDBDescriptor(ShapeDescriptor):
213    """
214        Descriptor for a PDB set of points
215       
216        Parameter:
217            - file = name of the PDB file
218    """
219    def __init__(self, filename):
220        """
221            Initialization
222            @param filename: name of the PDB file to load
223        """ 
224        ShapeDescriptor.__init__(self)
225        # Default parameters
226        self.params["type"]   = "pdb"
227        self.params["file"] = filename
228        self.params['is_lores'] = False
229
230    def create(self):
231        """
232            Create an instance of the shape
233            @return: instance of the shape
234        """
235        self.shapeObject = pointsmodelpy.new_pdbmodel()
236        pointsmodelpy.pdbmodel_add(self.shapeObject, self.params['file'])       
237       
238        #ShapeDescriptor.create(self)   
239        return self.shapeObject
240       
241# Define a dictionary for the shape until we find
242# a better way to create them
243shape_dict = {'sphere':SphereDescriptor,
244              'cylinder':CylinderDescriptor,
245              'ellipsoid':EllipsoidDescriptor,
246              'singlehelix':HelixDescriptor}
247       
248class VolumeCanvas(BaseComponent):
249    """
250        Class representing an empty space volume to add
251        geometrical object to.
252       
253        For 1D I(q) simulation, getPr() is called internally for the
254        first call to getIq().
255       
256    """
257   
258    def __init__(self):
259        """
260            Initialization
261        """
262        BaseComponent.__init__(self)
263       
264        ## Maximum value of q reachable
265        self.params['q_max'] = 0.1
266        self.params['lores_density'] = 0.1
267        self.params['scale'] = 1.0
268        self.params['background'] = 0.0
269       
270        self.lores_model = pointsmodelpy.new_loresmodel(self.params['lores_density'])
271        self.complex_model = pointsmodelpy.new_complexmodel()
272        self.shapes = {}
273        self.shapecount = 0       
274        self.points = None
275        self.npts = 0
276        self.hasPr = False       
277       
278    def _model_changed(self):
279        """
280            Reset internal data members to reflect the fact that the
281            real-space model has changed
282        """
283        self.hasPr  = False
284        self.points = None
285       
286    def addObject(self, shapeDesc, id = None):
287        """
288            Adds a real-space object to the canvas.
289       
290            @param shapeDesc: object to add to the canvas [ShapeDescriptor]
291            @param id: string handle for the object [string] [optional]
292            @return: string handle for the object
293        """
294        # If the handle is not provided, create one
295        if id == None:
296            id = shapeDesc.params["type"]+str(self.shapecount)
297         
298        # Self the order number
299        shapeDesc.params['order'] = self.shapecount
300        # Store the shape in a dictionary entry associated
301        # with the handle
302        self.shapes[id] = shapeDesc
303        self.shapecount += 1
304
305        #model changed, need to recalculate P(r)
306        self._model_changed()
307
308        return id
309           
310   
311    def add(self, shape, id = None):
312        """
313            The intend of this method is to eventually be able to use it
314            as a factory for the canvas and unify the simulation with the
315            analytical solutions. For instance, if one adds a cylinder and
316            it is the only shape on the canvas, the analytical solution
317            could be called. If multiple shapes are involved, then
318            simulation has to be performed.
319           
320            This function is deprecated, use addObject().
321       
322            @param shape: name of the object to add to the canvas [string]
323            @param id: string handle for the object [string] [optional]
324            @return: string handle for the object
325        """
326        # If the handle is not provided, create one
327        if id == None:
328            id = "shape"+str(self.shapecount)
329 
330        #shapeDesc = ShapeDescriptor(shape.lower())
331        if shape.lower() in shape_dict:
332            shapeDesc = shape_dict[shape.lower()]()
333        elif os.path.isfile(shape):
334            # A valid filename was supplier, create a PDB object
335            shapeDesc = PDBDescriptor(shape)
336        else:
337            raise ValueError, "VolumeCanvas.add: Unknown shape %s" % shape
338       
339        return self.addObject(shapeDesc, id)
340
341    def delete(self, id):
342        """
343            Delete a shape. The ID for the shape is required.
344            @param id: string handle for the object [string] [optional]
345        """
346
347        if self.shapes.has_key(id):
348            del self.shapes[id]
349        else:
350            raise KeyError, "VolumeCanvas.delete: could not find shape ID"
351
352        #model changed, need to recalculate P(r)
353        self._model_changed()
354
355
356    def setParam(self, name, value):   
357        """
358            Function to set the value of a parameter.
359            Both VolumeCanvas parameters and shape parameters
360            are accessible.
361           
362            Note: if shape parameters are accessed directly
363            from outside VolumeCanvas. The getPr method
364            should be called before evaluating I(q).
365       
366            TODO: implemented a check method to protect
367            against that.
368       
369            @param name: name of the parameter to change
370            @param value: value to give the parameter
371        """
372       
373        # Lowercase for case insensitivity
374        name = name.lower()
375       
376        # Look for shape access
377        toks = name.split('.')
378       
379        # If a shape identifier was given, look the shape up
380        # in the dictionary
381        if len(toks)>1:
382            if toks[0] in self.shapes.keys():
383                # The shape was found, now look for the parameter
384                if toks[1] in self.shapes[toks[0]].params:
385                    # The parameter was found, now change it
386                    self.shapes[toks[0]].params[toks[1]] = value
387                    self._model_changed()
388                else:
389                    raise ValueError, "Could not find parameter %s" % name
390            else:
391                raise ValueError, "Could not find shape %s" % toks[0]
392       
393        else:
394            # If we are not accessing the parameters of a
395            # shape, see if the parameter is part of this object
396            BaseComponent.setParam(self, name, value)
397            self._model_changed()
398
399    def getParam(self, name):   
400        """
401            @param name: name of the parameter to change
402        """
403        #TODO: clean this up
404       
405        # Lowercase for case insensitivity
406        name = name.lower()
407       
408        # Look for sub-model access
409        toks = name.split('.')
410        if len(toks) == 1:
411            try:
412                self.params.has_key(toks[0])
413            except KeyError:
414                raise ValueError, \
415                    "VolumeCanvas.getParam: Could not find %s" % name
416
417            value = self.params[toks[0]]
418            if isinstance(value, ShapeDescriptor):
419                raise ValueError, \
420                    "VolumeCanvas.getParam: Cannot get parameter value." 
421            else:
422                return value
423
424        elif len(toks) == 2:
425            try:
426                self.shapes.has_key(toks[0])
427            except KeyError:
428                raise ValueError, \
429                    "VolumeCanvas.getParam: Could not find %s" % name
430
431            shapeinstance = self.shapes[toks[0]]
432
433            try:
434                shapeinstance.params.has_key(toks[1])
435            except KeyError:
436                raise ValueError, \
437                    "VolumeCanvas.getParam: Could not find %s" % name
438
439            return shapeinstance.params[toks[1]]
440
441        else:
442            raise ValueError, \
443                "VolumeCanvas.getParam: Could not find %s" % name
444           
445    def getParamList(self, shapeid = None):
446        """
447               return a full list of all available parameters from
448           self.params.keys(). If a key in self.params is a instance
449           of ShapeDescriptor, extend the return list to:
450           [param1,param2,shapeid.param1,shapeid.param2.......]
451
452           If shapeid is provided, return the list of parameters that
453           belongs to that shape id only : [shapeid.param1, shapeid.param2...]
454        """
455
456        param_list = []
457        if shapeid == None:       
458            for key1 in self.params.keys():
459                #value1 = self.params[key1]
460                param_list.append(key1)
461            for key2 in self.shapes.keys():
462                value2 = self.shapes[key2]
463                header = key2 + '.'
464                for key3 in value2.params.keys():   
465                    fullname = header + key3                 
466                    param_list.append(fullname)
467     
468        else:
469            try:
470                self.shapes.has_key(shapeid)
471            except KeyError:
472                raise ValueError, \
473                    "VolumeCanvas: getParamList: Could not find %s" % shapeid
474            header = shapeid + '.'
475            param_list = self.shapes[shapeid].params.keys() 
476            for i in range(len(param_list)):
477                param_list[i] = header + param_list[i]
478
479        return param_list
480
481    def getShapeList(self):
482        """
483            Return a list of the shapes
484        """
485        return self.shapes.keys()
486
487    def _addSingleShape(self, shapeDesc):
488        """
489            create shapeobject based on shapeDesc
490            @param shapeDesc: shape description
491        """
492        #Create the object model
493        shapeDesc.create()
494                   
495        if shapeDesc.params['is_lores']:
496            # Add the shape to the lores_model
497            pointsmodelpy.lores_add(self.lores_model, 
498                shapeDesc.shapeObject, shapeDesc.params['contrast']) 
499
500    def _createVolumeFromList(self):
501        """
502            Create a new lores model with all the shapes in our internal list
503            Whenever we change a parameter of a shape, we have to re-create
504            the whole thing.
505           
506            Items with higher 'order' number take precedence for regions
507            of space that are shared with other objects. Points in the
508            overlapping region belonging to objects with lower 'order'
509            will be ignored.
510           
511            Items are added in decreasing 'order' number.
512            The item with the highest 'order' will be added *first*.
513            [That conventions is prescribed by the realSpaceModeling module]
514        """
515       
516        # Create empty model
517        self.lores_model = \
518            pointsmodelpy.new_loresmodel(self.params['lores_density'])
519
520        # Create empty complex model
521        self.complex_model = pointsmodelpy.new_complexmodel()
522       
523        # Order the object first
524        obj_list = []
525   
526        for shape in self.shapes:
527            order = self.shapes[shape].params['order']
528            # find where to place it in the list
529            stored = False
530           
531            for i in range(len(obj_list)):
532                if obj_list[i][0] > order:
533                    obj_list.insert(i, [order, shape])
534                    stored = True
535                    break
536           
537            if not stored:
538                obj_list.append([order, shape])
539               
540        # Add each shape
541        len_list = len(obj_list)
542        for i in range(len_list-1, -1, -1):
543            shapedesc = self.shapes[obj_list[i][1]]
544            self._addSingleShape(shapedesc)
545
546        return 0     
547   
548    def getPr(self):
549        """
550            Calculate P(r) from the objects on the canvas.
551            This method should always be called after the shapes
552            on the VolumeCanvas have changed.
553           
554            @return: calculation output flag
555        """
556        # To find a complete example of the correct call order:
557        # In LORES2, in actionclass.py, method CalculateAction._get_iq()
558       
559        # If there are not shapes, do nothing
560        if len(self.shapes) == 0:
561            self._model_changed()
562            return 0
563       
564        # generate space filling points from shape list
565        self._createVolumeFromList()
566
567        self.points = pointsmodelpy.new_point3dvec()
568
569        pointsmodelpy.complexmodel_add(self.complex_model, 
570                                        self.lores_model, "LORES")
571        for shape in self.shapes:
572            if self.shapes[shape].params['is_lores'] == False:
573                pointsmodelpy.complexmodel_add(self.complex_model, 
574                    self.shapes[shape].shapeObject, "PDB")
575       
576        #pointsmodelpy.get_lorespoints(self.lores_model, self.points)
577        self.npts = pointsmodelpy.get_complexpoints(self.complex_model, self.points)
578       
579        # expecting the rmax is a positive float or 0. The maximum distance.
580        #rmax = pointsmodelpy.get_lores_pr(self.lores_model, self.points)   
581         
582        rmax = pointsmodelpy.get_complex_pr(self.complex_model, self.points) 
583        self.hasPr = True   
584
585        return rmax
586       
587    def run(self, q = 0):
588        """
589            Returns the value of I(q) for a given q-value
590            @param q: q-value ([float] or [list]) ([A-1] or [[A-1], [rad]])
591            @return: I(q) [float] [cm-1]
592        """
593        # Check for 1D q length
594        if q.__class__.__name__ == 'int' \
595            or q.__class__.__name__ == 'float':
596            return self.getIq(q)
597        # Check for 2D q-value
598        elif q.__class__.__name__ == 'list':
599            # Compute (Qx, Qy) from (Q, phi)
600            # Phi is in radian and Q-values are in A-1
601            qx = q[0]*math.cos(q[1])
602            qy = q[0]*math.sin(q[1])
603            return self.getIq2D(qx, qy)
604        # Through an exception if it's not a
605        # type we recognize
606        else:
607            raise ValueError, "run(q): bad type for q"
608   
609    def runXY(self, q = 0):
610        """
611            Standard run command for the canvas.
612            Redirects to the correct method
613            according to the input type.
614            @param q: q-value [float] or [list] [A-1]
615            @return: I(q) [float] [cm-1]
616        """
617        # Check for 1D q length
618        if q.__class__.__name__ == 'int' \
619            or q.__class__.__name__ == 'float':
620            return self.getIq(q)
621        # Check for 2D q-value
622        elif q.__class__.__name__ == 'list':
623            return self.getIq2D(q[0], q[1])
624        # Through an exception if it's not a
625        # type we recognize
626        else:
627            raise ValueError, "runXY(q): bad type for q"
628   
629    def _create_modelObject(self):
630        """
631            Create the simulation model obejct from the list
632            of shapes.
633           
634            This method needs to be called each time a parameter
635            changes because of the way the underlying library
636            was (badly) written. It is impossible to change a
637            parameter, or remove a shape without having to
638            refill the space points.
639           
640            TODO: improve that.
641        """
642        # To find a complete example of the correct call order:
643        # In LORES2, in actionclass.py, method CalculateAction._get_iq()
644       
645        # If there are not shapes, do nothing
646        if len(self.shapes) == 0:
647            self._model_changed()
648            return 0
649       
650        # generate space filling points from shape list
651        self._createVolumeFromList()
652
653        self.points = pointsmodelpy.new_point3dvec()
654
655        pointsmodelpy.complexmodel_add(self.complex_model, 
656                                        self.lores_model, "LORES")
657        for shape in self.shapes:
658            if self.shapes[shape].params['is_lores'] == False:
659                pointsmodelpy.complexmodel_add(self.complex_model, 
660                    self.shapes[shape].shapeObject, "PDB")
661       
662        #pointsmodelpy.get_lorespoints(self.lores_model, self.points)
663        self.npts = pointsmodelpy.get_complexpoints(self.complex_model, self.points)
664       
665       
666    def getIq2D(self, qx, qy):
667        """
668            Returns simulate I(q) for given q_x and q_y values.
669            @param qx: q_x [A-1]
670            @param qy: q_y [A-1]
671            @return: I(q) [cm-1]
672        """
673       
674        # If this is the first simulation call, we need to generate the
675        # space points
676        if self.points == None:
677            self._create_modelObject()
678           
679            # Protect against empty model
680            if self.points == None:
681                return 0
682               
683        # Evalute I(q)
684        norm =  1.0e8/self.params['lores_density']*self.params['scale']
685        return norm*pointsmodelpy.get_complex_iq_2D(self.complex_model, self.points, qx, qy)\
686            + self.params['background']
687               
688    def write_pr(self, filename):
689        """
690            Write P(r) to an output file
691            @param filename: file name for P(r) output
692        """   
693        if self.hasPr == False:
694            self.getPr()
695     
696        pointsmodelpy.outputPR(self.complex_model, filename)
697     
698    def getPrData(self):
699        """
700            Write P(r) to an output file
701            @param filename: file name for P(r) output
702        """   
703        if self.hasPr == False:
704            self.getPr()
705     
706        return pointsmodelpy.get_pr(self.complex_model)
707     
708    def getIq(self, q):
709        """
710            Returns the value of I(q) for a given q-value
711           
712            This method should remain internal to the class
713            and the run() method should be used instead.
714           
715            @param q: q-value [float]
716            @return: I(q) [float]
717        """
718       
719        if self.hasPr == False:
720            self.getPr()
721
722        # By dividing by the density instead of the actuall V/N,
723        # we have an uncertainty of +-1 on N because the number
724        # of points chosen for the simulation is int(density*volume).
725        # Propagation of error gives:
726        #   delta(1/density^2) = 2*(1/density^2)/N
727        # where N is stored in self.npts
728
729        norm =  1.0e8/self.params['lores_density']*self.params['scale']
730        #return norm*pointsmodelpy.get_lores_i(self.lores_model, q)
731        return norm*pointsmodelpy.get_complex_i(self.complex_model, q)\
732            + self.params['background']
733   
734    def getError(self, q):
735        """
736            Returns the error of I(q) for a given q-value
737            @param q: q-value [float]
738            @return: I(q) [float]
739        """
740       
741        if self.hasPr == False:
742            self.getPr()
743
744        # By dividing by the density instead of the actual V/N,
745        # we have an uncertainty of +-1 on N because the number
746        # of points chosen for the simulation is int(density*volume).
747        # Propagation of error gives:
748        #   delta(1/density^2) = 2*(1/density^2)/N
749        # where N is stored in self.npts
750
751        norm =  1.0e8/self.params['lores_density']*self.params['scale']
752        #return norm*pointsmodelpy.get_lores_i(self.lores_model, q)
753        return norm*pointsmodelpy.get_complex_i_error(self.complex_model, q)\
754            + self.params['background']
755   
756    def getIqError(self, q):
757        """
758            Return the simulated value along with its estimated
759            error for a given q-value
760           
761            Propagation of errors is used to evaluate the
762            uncertainty.
763           
764            @param q: q-value [float]
765            @return: mean, error [float, float]
766        """
767        val = self.getIq(q)
768        # Simulation error (statistical)
769        err = self.getError(q)
770        # Error on V/N
771        simerr = 2*val/self.npts
772        return val, err+simerr
773
774    def getIq2DError(self, qx, qy):
775        """
776            Return the simulated value along with its estimated
777            error for a given q-value
778           
779            Propagation of errors is used to evaluate the
780            uncertainty.
781           
782            @param qx: qx-value [float]
783            @param qy: qy-value [float]
784            @return: mean, error [float, float]
785        """
786        self._create_modelObject()
787               
788        norm =  1.0e8/self.params['lores_density']*self.params['scale']
789        val = norm*pointsmodelpy.get_complex_iq_2D(self.complex_model, self.points, qx, qy)\
790            + self.params['background']
791       
792        # Simulation error (statistical)
793        norm =  1.0e8/self.params['lores_density']*self.params['scale'] \
794                * math.pow(self.npts/self.params['lores_density'], 1.0/3.0)/self.npts
795        err = norm*pointsmodelpy.get_complex_iq_2D_err(self.complex_model, self.points, qx, qy)
796        # Error on V/N
797        simerr = 2*val/self.npts
798       
799        # The error used for the position is over-simplified.
800        # The actual error was empirically found to be about
801        # an order of magnitude larger.
802        return val, 10.0*err+simerr
803       
Note: See TracBrowser for help on using the repository browser.