source: sasmodels/sasmodels/sasview_model.py @ 689b7c4

core_shell_microgelscostrafo411magnetic_modelticket-1257-vesicle-productticket_1156ticket_1265_superballticket_822_more_unit_tests
Last change on this file since 689b7c4 was 689b7c4, checked in by wojciech, 8 years ago

Removing try/except clause that wasn't properly handled

  • Property mode set to 100644
File size: 25.5 KB
Line 
1"""
2Sasview model constructor.
3
4Given a module defining an OpenCL kernel such as sasmodels.models.cylinder,
5create a sasview model class to run that kernel as follows::
6
7    from sasmodels.sasview_model import load_custom_model
8    CylinderModel = load_custom_model('sasmodels/models/cylinder.py')
9"""
10from __future__ import print_function
11
12import math
13from copy import deepcopy
14import collections
15import traceback
16import logging
17from os.path import basename, splitext
18
19import numpy as np  # type: ignore
20
21from . import core
22from . import custom
23from . import generate
24from . import weights
25from . import modelinfo
26from .details import make_kernel_args, dispersion_mesh
27
28try:
29    from typing import Dict, Mapping, Any, Sequence, Tuple, NamedTuple, List, Optional, Union, Callable
30    from .modelinfo import ModelInfo, Parameter
31    from .kernel import KernelModel
32    MultiplicityInfoType = NamedTuple(
33        'MuliplicityInfo',
34        [("number", int), ("control", str), ("choices", List[str]),
35         ("x_axis_label", str)])
36    SasviewModelType = Callable[[int], "SasviewModel"]
37except ImportError:
38    pass
39
40SUPPORT_OLD_STYLE_PLUGINS = True
41
42def _register_old_models():
43    # type: () -> None
44    """
45    Place the new models into sasview under the old names.
46
47    Monkey patch sas.sascalc.fit as sas.models so that sas.models.pluginmodel
48    is available to the plugin modules.
49    """
50    import sys
51    import sas
52    import sas.sascalc.fit
53    sys.modules['sas.models'] = sas.sascalc.fit
54    sas.models = sas.sascalc.fit
55
56    import sas.models
57    from sasmodels.conversion_table import CONVERSION_TABLE
58    for new_name, conversion in CONVERSION_TABLE.items():
59        old_name = conversion[0]
60        module_attrs = {old_name: find_model(new_name)}
61        ConstructedModule = type(old_name, (), module_attrs)
62        old_path = 'sas.models.' + old_name
63        setattr(sas.models, old_path, ConstructedModule)
64        sys.modules[old_path] = ConstructedModule
65
66
67# TODO: separate x_axis_label from multiplicity info
68MultiplicityInfo = collections.namedtuple(
69    'MultiplicityInfo',
70    ["number", "control", "choices", "x_axis_label"],
71)
72
73MODELS = {}
74def find_model(modelname):
75    # type: (str) -> SasviewModelType
76    """
77    Find a model by name.  If the model name ends in py, try loading it from
78    custom models, otherwise look for it in the list of builtin models.
79    """
80    # TODO: used by sum/product model to load an existing model
81    # TODO: doesn't handle custom models properly
82    if modelname.endswith('.py'):
83        return load_custom_model(modelname)
84    elif modelname in MODELS:
85        return MODELS[modelname]
86    else:
87        raise ValueError("unknown model %r"%modelname)
88
89
90# TODO: figure out how to say that the return type is a subclass
91def load_standard_models():
92    # type: () -> List[SasviewModelType]
93    """
94    Load and return the list of predefined models.
95
96    If there is an error loading a model, then a traceback is logged and the
97    model is not returned.
98    """
99    models = []
100    for name in core.list_models():
101        try:
102            MODELS[name] = _make_standard_model(name)
103            models.append(MODELS[name])
104        except Exception:
105            logging.error(traceback.format_exc())
106    if SUPPORT_OLD_STYLE_PLUGINS:
107        _register_old_models()
108
109    return models
110
111
112def load_custom_model(path):
113    # type: (str) -> SasviewModelType
114    """
115    Load a custom model given the model path.
116    """
117    kernel_module = custom.load_custom_kernel_module(path)
118    try:
119        model = kernel_module.Model
120        # Old style models do not set the name in the class attributes, so
121        # set it here; this name will be overridden when the object is created
122        # with an instance variable that has the same value.
123        if model.name == "":
124            model.name = splitext(basename(path))[0]
125        if not hasattr(model, 'filename'):
126            model.filename = kernel_module.__file__
127    except AttributeError:
128        model_info = modelinfo.make_model_info(kernel_module)
129        model = _make_model_from_info(model_info)
130
131    # If a model name already exists and we are loading a different model,
132    # use the model file name as the model name.
133    if model.name in MODELS and not model.filename == MODELS[model.name].filename:
134        _previous_name = model.name
135        model.name = model.id
136       
137        # If the new model name is still in the model list (for instance,
138        # if we put a cylinder.py in our plug-in directory), then append
139        # an identifier.
140        if model.name in MODELS and not model.filename == MODELS[model.name].filename:
141            model.name = model.id + '_user'
142        logging.info("Model %s already exists: using %s [%s]", _previous_name, model.name, model.filename)
143
144    MODELS[model.name] = model
145    return model
146
147
148def _make_standard_model(name):
149    # type: (str) -> SasviewModelType
150    """
151    Load the sasview model defined by *name*.
152
153    *name* can be a standard model name or a path to a custom model.
154
155    Returns a class that can be used directly as a sasview model.
156    """
157    kernel_module = generate.load_kernel_module(name)
158    model_info = modelinfo.make_model_info(kernel_module)
159    return _make_model_from_info(model_info)
160
161
162def _make_model_from_info(model_info):
163    # type: (ModelInfo) -> SasviewModelType
164    """
165    Convert *model_info* into a SasView model wrapper.
166    """
167    def __init__(self, multiplicity=None):
168        SasviewModel.__init__(self, multiplicity=multiplicity)
169    attrs = _generate_model_attributes(model_info)
170    attrs['__init__'] = __init__
171    attrs['filename'] = model_info.filename
172    ConstructedModel = type(model_info.name, (SasviewModel,), attrs) # type: SasviewModelType
173    return ConstructedModel
174
175def _generate_model_attributes(model_info):
176    # type: (ModelInfo) -> Dict[str, Any]
177    """
178    Generate the class attributes for the model.
179
180    This should include all the information necessary to query the model
181    details so that you do not need to instantiate a model to query it.
182
183    All the attributes should be immutable to avoid accidents.
184    """
185
186    # TODO: allow model to override axis labels input/output name/unit
187
188    # Process multiplicity
189    non_fittable = []  # type: List[str]
190    xlabel = model_info.profile_axes[0] if model_info.profile is not None else ""
191    variants = MultiplicityInfo(0, "", [], xlabel)
192    for p in model_info.parameters.kernel_parameters:
193        if p.name == model_info.control:
194            non_fittable.append(p.name)
195            variants = MultiplicityInfo(
196                len(p.choices) if p.choices else int(p.limits[1]),
197                p.name, p.choices, xlabel
198            )
199            break
200
201    # Only a single drop-down list parameter available
202    fun_list = []
203    for p in model_info.parameters.kernel_parameters:
204        if p.choices:
205            fun_list = p.choices
206            if p.length > 1:
207                non_fittable.extend(p.id+str(k) for k in range(1, p.length+1))
208            break
209
210    # Organize parameter sets
211    orientation_params = []
212    magnetic_params = []
213    fixed = []
214    for p in model_info.parameters.user_parameters():
215        if p.type == 'orientation':
216            orientation_params.append(p.name)
217            orientation_params.append(p.name+".width")
218            fixed.append(p.name+".width")
219        elif p.type == 'magnetic':
220            orientation_params.append(p.name)
221            magnetic_params.append(p.name)
222            fixed.append(p.name+".width")
223
224
225    # Build class dictionary
226    attrs = {}  # type: Dict[str, Any]
227    attrs['_model_info'] = model_info
228    attrs['name'] = model_info.name
229    attrs['id'] = model_info.id
230    attrs['description'] = model_info.description
231    attrs['category'] = model_info.category
232    attrs['is_structure_factor'] = model_info.structure_factor
233    attrs['is_form_factor'] = model_info.ER is not None
234    attrs['is_multiplicity_model'] = variants[0] > 1
235    attrs['multiplicity_info'] = variants
236    attrs['orientation_params'] = tuple(orientation_params)
237    attrs['magnetic_params'] = tuple(magnetic_params)
238    attrs['fixed'] = tuple(fixed)
239    attrs['non_fittable'] = tuple(non_fittable)
240    attrs['fun_list'] = tuple(fun_list)
241
242    return attrs
243
244class SasviewModel(object):
245    """
246    Sasview wrapper for opencl/ctypes model.
247    """
248    # Model parameters for the specific model are set in the class constructor
249    # via the _generate_model_attributes function, which subclasses
250    # SasviewModel.  They are included here for typing and documentation
251    # purposes.
252    _model = None       # type: KernelModel
253    _model_info = None  # type: ModelInfo
254    #: load/save name for the model
255    id = None           # type: str
256    #: display name for the model
257    name = None         # type: str
258    #: short model description
259    description = None  # type: str
260    #: default model category
261    category = None     # type: str
262
263    #: names of the orientation parameters in the order they appear
264    orientation_params = None # type: Sequence[str]
265    #: names of the magnetic parameters in the order they appear
266    magnetic_params = None    # type: Sequence[str]
267    #: names of the fittable parameters
268    fixed = None              # type: Sequence[str]
269    # TODO: the attribute fixed is ill-named
270
271    # Axis labels
272    input_name = "Q"
273    input_unit = "A^{-1}"
274    output_name = "Intensity"
275    output_unit = "cm^{-1}"
276
277    #: default cutoff for polydispersity
278    cutoff = 1e-5
279
280    # Note: Use non-mutable values for class attributes to avoid errors
281    #: parameters that are not fitted
282    non_fittable = ()        # type: Sequence[str]
283
284    #: True if model should appear as a structure factor
285    is_structure_factor = False
286    #: True if model should appear as a form factor
287    is_form_factor = False
288    #: True if model has multiplicity
289    is_multiplicity_model = False
290    #: Mulitplicity information
291    multiplicity_info = None # type: MultiplicityInfoType
292
293    # Per-instance variables
294    #: parameter {name: value} mapping
295    params = None      # type: Dict[str, float]
296    #: values for dispersion width, npts, nsigmas and type
297    dispersion = None  # type: Dict[str, Any]
298    #: units and limits for each parameter
299    details = None     # type: Dict[str, Sequence[Any]]
300    #                  # actual type is Dict[str, List[str, float, float]]
301    #: multiplicity value, or None if no multiplicity on the model
302    multiplicity = None     # type: Optional[int]
303    #: memory for polydispersity array if using ArrayDispersion (used by sasview).
304    _persistency_dict = None # type: Dict[str, Tuple[np.ndarray, np.ndarray]]
305
306    def __init__(self, multiplicity=None):
307        # type: (Optional[int]) -> None
308
309        # TODO: _persistency_dict to persistency_dict throughout sasview
310        # TODO: refactor multiplicity to encompass variants
311        # TODO: dispersion should be a class
312        # TODO: refactor multiplicity info
313        # TODO: separate profile view from multiplicity
314        # The button label, x and y axis labels and scale need to be under
315        # the control of the model, not the fit page.  Maximum flexibility,
316        # the fit page would supply the canvas and the profile could plot
317        # how it wants, but this assumes matplotlib.  Next level is that
318        # we provide some sort of data description including title, labels
319        # and lines to plot.
320
321        # Get the list of hidden parameters given the mulitplicity
322        # Don't include multiplicity in the list of parameters
323        self.multiplicity = multiplicity
324        if multiplicity is not None:
325            hidden = self._model_info.get_hidden_parameters(multiplicity)
326            hidden |= set([self.multiplicity_info.control])
327        else:
328            hidden = set()
329        if self._model_info.structure_factor:
330            hidden.add('scale')
331            hidden.add('background')
332            self._model_info.parameters.defaults['background'] = 0.
333
334        self._persistency_dict = {}
335        self.params = collections.OrderedDict()
336        self.dispersion = collections.OrderedDict()
337        self.details = {}
338        for p in self._model_info.parameters.user_parameters():
339            if p.name in hidden:
340                continue
341            self.params[p.name] = p.default
342            self.details[p.id] = [p.units, p.limits[0], p.limits[1]]
343            if p.polydisperse:
344                self.details[p.id+".width"] = [
345                    "", 0.0, 1.0 if p.relative_pd else np.inf
346                ]
347                self.dispersion[p.name] = {
348                    'width': 0,
349                    'npts': 35,
350                    'nsigmas': 3,
351                    'type': 'gaussian',
352                }
353
354    def __get_state__(self):
355        # type: () -> Dict[str, Any]
356        state = self.__dict__.copy()
357        state.pop('_model')
358        # May need to reload model info on set state since it has pointers
359        # to python implementations of Iq, etc.
360        #state.pop('_model_info')
361        return state
362
363    def __set_state__(self, state):
364        # type: (Dict[str, Any]) -> None
365        self.__dict__ = state
366        self._model = None
367
368    def __str__(self):
369        # type: () -> str
370        """
371        :return: string representation
372        """
373        return self.name
374
375    def is_fittable(self, par_name):
376        # type: (str) -> bool
377        """
378        Check if a given parameter is fittable or not
379
380        :param par_name: the parameter name to check
381        """
382        return par_name in self.fixed
383        #For the future
384        #return self.params[str(par_name)].is_fittable()
385
386
387    def getProfile(self):
388        # type: () -> (np.ndarray, np.ndarray)
389        """
390        Get SLD profile
391
392        : return: (z, beta) where z is a list of depth of the transition points
393                beta is a list of the corresponding SLD values
394        """
395        args = {} # type: Dict[str, Any]
396        for p in self._model_info.parameters.kernel_parameters:
397            if p.id == self.multiplicity_info.control:
398                value = float(self.multiplicity)
399            elif p.length == 1:
400                value = self.params.get(p.id, np.NaN)
401            else:
402                value = np.array([self.params.get(p.id+str(k), np.NaN)
403                                  for k in range(1, p.length+1)])
404            args[p.id] = value
405
406        x, y = self._model_info.profile(**args)
407        return x, 1e-6*y
408
409    def setParam(self, name, value):
410        # type: (str, float) -> None
411        """
412        Set the value of a model parameter
413
414        :param name: name of the parameter
415        :param value: value of the parameter
416
417        """
418        # Look for dispersion parameters
419        toks = name.split('.')
420        if len(toks) == 2:
421            for item in self.dispersion.keys():
422                if item == toks[0]:
423                    for par in self.dispersion[item]:
424                        if par == toks[1]:
425                            self.dispersion[item][par] = value
426                            return
427        else:
428            # Look for standard parameter
429            for item in self.params.keys():
430                if item == name:
431                    self.params[item] = value
432                    return
433
434        raise ValueError("Model does not contain parameter %s" % name)
435
436    def getParam(self, name):
437        # type: (str) -> float
438        """
439        Set the value of a model parameter
440
441        :param name: name of the parameter
442
443        """
444        # Look for dispersion parameters
445        toks = name.split('.')
446        if len(toks) == 2:
447            for item in self.dispersion.keys():
448                if item == toks[0]:
449                    for par in self.dispersion[item]:
450                        if par == toks[1]:
451                            return self.dispersion[item][par]
452        else:
453            # Look for standard parameter
454            for item in self.params.keys():
455                if item == name:
456                    return self.params[item]
457
458        raise ValueError("Model does not contain parameter %s" % name)
459
460    def getParamList(self):
461        # type: () -> Sequence[str]
462        """
463        Return a list of all available parameters for the model
464        """
465        param_list = list(self.params.keys())
466        # WARNING: Extending the list with the dispersion parameters
467        param_list.extend(self.getDispParamList())
468        return param_list
469
470    def getDispParamList(self):
471        # type: () -> Sequence[str]
472        """
473        Return a list of polydispersity parameters for the model
474        """
475        # TODO: fix test so that parameter order doesn't matter
476        ret = ['%s.%s' % (p_name, ext)
477               for p_name in self.dispersion.keys()
478               for ext in ('npts', 'nsigmas', 'width')]
479        #print(ret)
480        return ret
481
482    def clone(self):
483        # type: () -> "SasviewModel"
484        """ Return a identical copy of self """
485        return deepcopy(self)
486
487    def run(self, x=0.0):
488        # type: (Union[float, (float, float), List[float]]) -> float
489        """
490        Evaluate the model
491
492        :param x: input q, or [q,phi]
493
494        :return: scattering function P(q)
495
496        **DEPRECATED**: use calculate_Iq instead
497        """
498        if isinstance(x, (list, tuple)):
499            # pylint: disable=unpacking-non-sequence
500            q, phi = x
501            return self.calculate_Iq([q*math.cos(phi)], [q*math.sin(phi)])[0]
502        else:
503            return self.calculate_Iq([x])[0]
504
505
506    def runXY(self, x=0.0):
507        # type: (Union[float, (float, float), List[float]]) -> float
508        """
509        Evaluate the model in cartesian coordinates
510
511        :param x: input q, or [qx, qy]
512
513        :return: scattering function P(q)
514
515        **DEPRECATED**: use calculate_Iq instead
516        """
517        if isinstance(x, (list, tuple)):
518            return self.calculate_Iq([x[0]], [x[1]])[0]
519        else:
520            return self.calculate_Iq([x])[0]
521
522    def evalDistribution(self, qdist):
523        # type: (Union[np.ndarray, Tuple[np.ndarray, np.ndarray], List[np.ndarray]]) -> np.ndarray
524        r"""
525        Evaluate a distribution of q-values.
526
527        :param qdist: array of q or a list of arrays [qx,qy]
528
529        * For 1D, a numpy array is expected as input
530
531        ::
532
533            evalDistribution(q)
534
535          where *q* is a numpy array.
536
537        * For 2D, a list of *[qx,qy]* is expected with 1D arrays as input
538
539        ::
540
541              qx = [ qx[0], qx[1], qx[2], ....]
542              qy = [ qy[0], qy[1], qy[2], ....]
543
544        If the model is 1D only, then
545
546        .. math::
547
548            q = \sqrt{q_x^2+q_y^2}
549
550        """
551        if isinstance(qdist, (list, tuple)):
552            # Check whether we have a list of ndarrays [qx,qy]
553            qx, qy = qdist
554            if not self._model_info.parameters.has_2d:
555                return self.calculate_Iq(np.sqrt(qx ** 2 + qy ** 2))
556            else:
557                return self.calculate_Iq(qx, qy)
558
559        elif isinstance(qdist, np.ndarray):
560            # We have a simple 1D distribution of q-values
561            return self.calculate_Iq(qdist)
562
563        else:
564            raise TypeError("evalDistribution expects q or [qx, qy], not %r"
565                            % type(qdist))
566
567    def calculate_Iq(self, qx, qy=None):
568        # type: (Sequence[float], Optional[Sequence[float]]) -> np.ndarray
569        """
570        Calculate Iq for one set of q with the current parameters.
571
572        If the model is 1D, use *q*.  If 2D, use *qx*, *qy*.
573
574        This should NOT be used for fitting since it copies the *q* vectors
575        to the card for each evaluation.
576        """
577        #core.HAVE_OPENCL = False
578        if self._model is None:
579            self._model = core.build_model(self._model_info)
580        if qy is not None:
581            q_vectors = [np.asarray(qx), np.asarray(qy)]
582        else:
583            q_vectors = [np.asarray(qx)]
584        calculator = self._model.make_kernel(q_vectors)
585        parameters = self._model_info.parameters
586        pairs = [self._get_weights(p) for p in parameters.call_parameters]
587        #weights.plot_weights(self._model_info, pairs)
588        call_details, values, is_magnetic = make_kernel_args(calculator, pairs)
589        #call_details.show()
590        #print("pairs", pairs)
591        #print("params", self.params)
592        #print("values", values)
593        #print("is_mag", is_magnetic)
594        result = calculator(call_details, values, cutoff=self.cutoff,
595                            magnetic=is_magnetic)
596        calculator.release()
597        self._model.release()
598        return result
599
600    def calculate_ER(self):
601        # type: () -> float
602        """
603        Calculate the effective radius for P(q)*S(q)
604
605        :return: the value of the effective radius
606        """
607        if self._model_info.ER is None:
608            return 1.0
609        else:
610            value, weight = self._dispersion_mesh()
611            fv = self._model_info.ER(*value)
612            #print(values[0].shape, weights.shape, fv.shape)
613            return np.sum(weight * fv) / np.sum(weight)
614
615    def calculate_VR(self):
616        # type: () -> float
617        """
618        Calculate the volf ratio for P(q)*S(q)
619
620        :return: the value of the volf ratio
621        """
622        if self._model_info.VR is None:
623            return 1.0
624        else:
625            value, weight = self._dispersion_mesh()
626            whole, part = self._model_info.VR(*value)
627            return np.sum(weight * part) / np.sum(weight * whole)
628
629    def set_dispersion(self, parameter, dispersion):
630        # type: (str, weights.Dispersion) -> Dict[str, Any]
631        """
632        Set the dispersion object for a model parameter
633
634        :param parameter: name of the parameter [string]
635        :param dispersion: dispersion object of type Dispersion
636        """
637        if parameter in self.params:
638            # TODO: Store the disperser object directly in the model.
639            # The current method of relying on the sasview GUI to
640            # remember them is kind of funky.
641            # Note: can't seem to get disperser parameters from sasview
642            # (1) Could create a sasview model that has not yet been
643            # converted, assign the disperser to one of its polydisperse
644            # parameters, then retrieve the disperser parameters from the
645            # sasview model.
646            # (2) Could write a disperser parameter retriever in sasview.
647            # (3) Could modify sasview to use sasmodels.weights dispersers.
648            # For now, rely on the fact that the sasview only ever uses
649            # new dispersers in the set_dispersion call and create a new
650            # one instead of trying to assign parameters.
651            self.dispersion[parameter] = dispersion.get_pars()
652        else:
653            raise ValueError("%r is not a dispersity or orientation parameter")
654
655    def _dispersion_mesh(self):
656        # type: () -> List[Tuple[np.ndarray, np.ndarray]]
657        """
658        Create a mesh grid of dispersion parameters and weights.
659
660        Returns [p1,p2,...],w where pj is a vector of values for parameter j
661        and w is a vector containing the products for weights for each
662        parameter set in the vector.
663        """
664        pars = [self._get_weights(p)
665                for p in self._model_info.parameters.call_parameters
666                if p.type == 'volume']
667        return dispersion_mesh(self._model_info, pars)
668
669    def _get_weights(self, par):
670        # type: (Parameter) -> Tuple[np.ndarray, np.ndarray]
671        """
672        Return dispersion weights for parameter
673        """
674        if par.name not in self.params:
675            if par.name == self.multiplicity_info.control:
676                return [self.multiplicity], [1.0]
677            else:
678                # For hidden parameters use the default value.
679                value = self._model_info.parameters.defaults.get(par.name, np.NaN)
680                return [value], [1.0]
681        elif par.polydisperse:
682            dis = self.dispersion[par.name]
683            if dis['type'] == 'array':
684                value, weight = dis['values'], dis['weights']
685            else:
686                value, weight = weights.get_weights(
687                    dis['type'], dis['npts'], dis['width'], dis['nsigmas'],
688                    self.params[par.name], par.limits, par.relative_pd)
689            return value, weight / np.sum(weight)
690        else:
691            return [self.params[par.name]], [1.0]
692
693def test_model():
694    # type: () -> float
695    """
696    Test that a sasview model (cylinder) can be run.
697    """
698    Cylinder = _make_standard_model('cylinder')
699    cylinder = Cylinder()
700    return cylinder.evalDistribution([0.1, 0.1])
701
702def test_structure_factor():
703    # type: () -> float
704    """
705    Test that a sasview model (cylinder) can be run.
706    """
707    Model = _make_standard_model('hardsphere')
708    model = Model()
709    value = model.evalDistribution([0.1, 0.1])
710    if np.isnan(value):
711        raise ValueError("hardsphere returns null")
712
713def test_rpa():
714    # type: () -> float
715    """
716    Test that a sasview model (cylinder) can be run.
717    """
718    RPA = _make_standard_model('rpa')
719    rpa = RPA(3)
720    return rpa.evalDistribution([0.1, 0.1])
721
722
723def test_model_list():
724    # type: () -> None
725    """
726    Make sure that all models build as sasview models.
727    """
728    from .exception import annotate_exception
729    for name in core.list_models():
730        try:
731            _make_standard_model(name)
732        except:
733            annotate_exception("when loading "+name)
734            raise
735
736def test_old_name():
737    # type: () -> None
738    """
739    Load and run cylinder model from sas.models.CylinderModel
740    """
741    if not SUPPORT_OLD_STYLE_PLUGINS:
742        return
743    try:
744        # if sasview is not on the path then don't try to test it
745        import sas
746    except ImportError:
747        return
748    load_standard_models()
749    from sas.models.CylinderModel import CylinderModel
750    CylinderModel().evalDistribution([0.1, 0.1])
751
752if __name__ == "__main__":
753    print("cylinder(0.1,0.1)=%g"%test_model())
Note: See TracBrowser for help on using the repository browser.