source: sasmodels/sasmodels/sasview_model.py @ 2f2c70c

core_shell_microgelscostrafo411magnetic_modelticket-1257-vesicle-productticket_1156ticket_1265_superballticket_822_more_unit_tests
Last change on this file since 2f2c70c was 2f2c70c, checked in by mathieu, 8 years ago

Fix problem with reloading. Re #673

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