source: sasmodels/sasmodels/compare.py @ 73e08ae

core_shell_microgelscostrafo411magnetic_modelticket-1257-vesicle-productticket_1156ticket_1265_superballticket_822_more_unit_tests
Last change on this file since 73e08ae was 248561a, checked in by Paul Kienzle <pkienzle@…>, 8 years ago

allow math functions such as sqrt and atan in parameter expressions

  • Property mode set to 100755
File size: 39.4 KB
RevLine 
[8a20be5]1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
[caeb06d]3"""
4Program to compare models using different compute engines.
5
6This program lets you compare results between OpenCL and DLL versions
7of the code and between precision (half, fast, single, double, quad),
8where fast precision is single precision using native functions for
9trig, etc., and may not be completely IEEE 754 compliant.  This lets
10make sure that the model calculations are stable, or if you need to
[9cfcac8]11tag the model as double precision only.
[caeb06d]12
[9cfcac8]13Run using ./compare.sh (Linux, Mac) or compare.bat (Windows) in the
[caeb06d]14sasmodels root to see the command line options.
15
[9cfcac8]16Note that there is no way within sasmodels to select between an
17OpenCL CPU device and a GPU device, but you can do so by setting the
[caeb06d]18PYOPENCL_CTX environment variable ahead of time.  Start a python
19interpreter and enter::
20
21    import pyopencl as cl
22    cl.create_some_context()
23
24This will prompt you to select from the available OpenCL devices
25and tell you which string to use for the PYOPENCL_CTX variable.
26On Windows you will need to remove the quotes.
27"""
28
29from __future__ import print_function
30
[190fc2b]31import sys
32import math
33import datetime
34import traceback
[ff1fff5]35import re
[190fc2b]36
[7ae2b7f]37import numpy as np  # type: ignore
[190fc2b]38
39from . import core
40from . import kerneldll
[6831fa0]41from . import exception
[190fc2b]42from .data import plot_theory, empty_data1D, empty_data2D
43from .direct_model import DirectModel
[f247314]44from .convert import revert_name, revert_pars, constrain_new_to_old
[ff1fff5]45from .generate import FLOAT_RE
[190fc2b]46
[dd7fc12]47try:
48    from typing import Optional, Dict, Any, Callable, Tuple
[6831fa0]49except Exception:
[dd7fc12]50    pass
51else:
52    from .modelinfo import ModelInfo, Parameter, ParameterSet
53    from .data import Data
[8d62008]54    Calculator = Callable[[float], np.ndarray]
[dd7fc12]55
[caeb06d]56USAGE = """
57usage: compare.py model N1 N2 [options...] [key=val]
58
59Compare the speed and value for a model between the SasView original and the
60sasmodels rewrite.
61
62model is the name of the model to compare (see below).
63N1 is the number of times to run sasmodels (default=1).
64N2 is the number times to run sasview (default=1).
65
66Options (* for default):
67
68    -plot*/-noplot plots or suppress the plot of the model
69    -lowq*/-midq/-highq/-exq use q values up to 0.05, 0.2, 1.0, 10.0
70    -nq=128 sets the number of Q points in the data set
[e78edc4]71    -zero indicates that q=0 should be included
[caeb06d]72    -1d*/-2d computes 1d or 2d data
73    -preset*/-random[=seed] preset or random parameters
74    -mono/-poly* force monodisperse/polydisperse
75    -cutoff=1e-5* cutoff value for including a point in polydispersity
76    -pars/-nopars* prints the parameter set or not
77    -abs/-rel* plot relative or absolute error
78    -linear/-log*/-q4 intensity scaling
79    -hist/-nohist* plot histogram of relative error
80    -res=0 sets the resolution width dQ/Q if calculating with resolution
81    -accuracy=Low accuracy of the resolution calculation Low, Mid, High, Xhigh
82    -edit starts the parameter explorer
[98d6cfc]83    -default/-demo* use demo vs default parameters
[234c532]84    -html shows the model docs instead of running the model
[9068f4c]85    -title="note" adds note to the plot title, after the model name
[caeb06d]86
87Any two calculation engines can be selected for comparison:
88
89    -single/-double/-half/-fast sets an OpenCL calculation engine
90    -single!/-double!/-quad! sets an OpenMP calculation engine
91    -sasview sets the sasview calculation engine
92
93The default is -single -sasview.  Note that the interpretation of quad
94precision depends on architecture, and may vary from 64-bit to 128-bit,
95with 80-bit floats being common (1e-19 precision).
96
97Key=value pairs allow you to set specific values for the model parameters.
98"""
99
100# Update docs with command line usage string.   This is separate from the usual
101# doc string so that we can display it at run time if there is an error.
102# lin
[d15a908]103__doc__ = (__doc__  # pylint: disable=redefined-builtin
104           + """
[caeb06d]105Program description
106-------------------
107
[d15a908]108"""
109           + USAGE)
[caeb06d]110
[750ffa5]111kerneldll.ALLOW_SINGLE_PRECISION_DLLS = True
[87985ca]112
[248561a]113# list of math functions for use in evaluating parameters
114MATH = dict((k,getattr(math, k)) for k in dir(math) if not k.startswith('_'))
115
[7cf2cfd]116# CRUFT python 2.6
117if not hasattr(datetime.timedelta, 'total_seconds'):
118    def delay(dt):
119        """Return number date-time delta as number seconds"""
120        return dt.days * 86400 + dt.seconds + 1e-6 * dt.microseconds
121else:
122    def delay(dt):
123        """Return number date-time delta as number seconds"""
124        return dt.total_seconds()
125
126
[4f2478e]127class push_seed(object):
128    """
129    Set the seed value for the random number generator.
130
131    When used in a with statement, the random number generator state is
132    restored after the with statement is complete.
133
134    :Parameters:
135
136    *seed* : int or array_like, optional
137        Seed for RandomState
138
139    :Example:
140
141    Seed can be used directly to set the seed::
142
143        >>> from numpy.random import randint
144        >>> push_seed(24)
145        <...push_seed object at...>
146        >>> print(randint(0,1000000,3))
147        [242082    899 211136]
148
149    Seed can also be used in a with statement, which sets the random
150    number generator state for the enclosed computations and restores
151    it to the previous state on completion::
152
153        >>> with push_seed(24):
154        ...    print(randint(0,1000000,3))
155        [242082    899 211136]
156
157    Using nested contexts, we can demonstrate that state is indeed
158    restored after the block completes::
159
160        >>> with push_seed(24):
161        ...    print(randint(0,1000000))
162        ...    with push_seed(24):
163        ...        print(randint(0,1000000,3))
164        ...    print(randint(0,1000000))
165        242082
166        [242082    899 211136]
167        899
168
169    The restore step is protected against exceptions in the block::
170
171        >>> with push_seed(24):
172        ...    print(randint(0,1000000))
173        ...    try:
174        ...        with push_seed(24):
175        ...            print(randint(0,1000000,3))
176        ...            raise Exception()
[dd7fc12]177        ...    except Exception:
[4f2478e]178        ...        print("Exception raised")
179        ...    print(randint(0,1000000))
180        242082
181        [242082    899 211136]
182        Exception raised
183        899
184    """
185    def __init__(self, seed=None):
[dd7fc12]186        # type: (Optional[int]) -> None
[4f2478e]187        self._state = np.random.get_state()
188        np.random.seed(seed)
189
190    def __enter__(self):
[dd7fc12]191        # type: () -> None
192        pass
[4f2478e]193
[b32dafd]194    def __exit__(self, exc_type, exc_value, traceback):
[dd7fc12]195        # type: (Any, BaseException, Any) -> None
196        # TODO: better typing for __exit__ method
[4f2478e]197        np.random.set_state(self._state)
198
[7cf2cfd]199def tic():
[dd7fc12]200    # type: () -> Callable[[], float]
[7cf2cfd]201    """
202    Timer function.
203
204    Use "toc=tic()" to start the clock and "toc()" to measure
205    a time interval.
206    """
207    then = datetime.datetime.now()
208    return lambda: delay(datetime.datetime.now() - then)
209
210
211def set_beam_stop(data, radius, outer=None):
[dd7fc12]212    # type: (Data, float, float) -> None
[7cf2cfd]213    """
214    Add a beam stop of the given *radius*.  If *outer*, make an annulus.
215
[dd7fc12]216    Note: this function does not require sasview
[7cf2cfd]217    """
218    if hasattr(data, 'qx_data'):
219        q = np.sqrt(data.qx_data**2 + data.qy_data**2)
220        data.mask = (q < radius)
221        if outer is not None:
222            data.mask |= (q >= outer)
223    else:
224        data.mask = (data.x < radius)
225        if outer is not None:
226            data.mask |= (data.x >= outer)
227
[8a20be5]228
[ec7e360]229def parameter_range(p, v):
[dd7fc12]230    # type: (str, float) -> Tuple[float, float]
[87985ca]231    """
[ec7e360]232    Choose a parameter range based on parameter name and initial value.
[87985ca]233    """
[8bd7b77]234    # process the polydispersity options
[ec7e360]235    if p.endswith('_pd_n'):
[dd7fc12]236        return 0., 100.
[ec7e360]237    elif p.endswith('_pd_nsigma'):
[dd7fc12]238        return 0., 5.
[ec7e360]239    elif p.endswith('_pd_type'):
[dd7fc12]240        raise ValueError("Cannot return a range for a string value")
[caeb06d]241    elif any(s in p for s in ('theta', 'phi', 'psi')):
[87985ca]242        # orientation in [-180,180], orientation pd in [0,45]
243        if p.endswith('_pd'):
[dd7fc12]244            return 0., 45.
[87985ca]245        else:
[dd7fc12]246            return -180., 180.
[87985ca]247    elif p.endswith('_pd'):
[dd7fc12]248        return 0., 1.
[8bd7b77]249    elif 'sld' in p:
[dd7fc12]250        return -0.5, 10.
[eb46451]251    elif p == 'background':
[dd7fc12]252        return 0., 10.
[eb46451]253    elif p == 'scale':
[dd7fc12]254        return 0., 1.e3
255    elif v < 0.:
256        return 2.*v, -2.*v
[87985ca]257    else:
[dd7fc12]258        return 0., (2.*v if v > 0. else 1.)
[87985ca]259
[4f2478e]260
[8bd7b77]261def _randomize_one(model_info, p, v):
[dd7fc12]262    # type: (ModelInfo, str, float) -> float
263    # type: (ModelInfo, str, str) -> str
[ec7e360]264    """
[caeb06d]265    Randomize a single parameter.
[ec7e360]266    """
[f3bd37f]267    if any(p.endswith(s) for s in ('_pd', '_pd_n', '_pd_nsigma', '_pd_type')):
[ec7e360]268        return v
[8bd7b77]269
270    # Find the parameter definition
[6d6508e]271    for par in model_info.parameters.call_parameters:
[8bd7b77]272        if par.name == p:
273            break
[ec7e360]274    else:
[8bd7b77]275        raise ValueError("unknown parameter %r"%p)
276
277    if len(par.limits) > 2:  # choice list
278        return np.random.randint(len(par.limits))
279
280    limits = par.limits
281    if not np.isfinite(limits).all():
282        low, high = parameter_range(p, v)
283        limits = (max(limits[0], low), min(limits[1], high))
284
285    return np.random.uniform(*limits)
[cd3dba0]286
[4f2478e]287
[8bd7b77]288def randomize_pars(model_info, pars, seed=None):
[dd7fc12]289    # type: (ModelInfo, ParameterSet, int) -> ParameterSet
[caeb06d]290    """
291    Generate random values for all of the parameters.
292
293    Valid ranges for the random number generator are guessed from the name of
294    the parameter; this will not account for constraints such as cap radius
295    greater than cylinder radius in the capped_cylinder model, so
296    :func:`constrain_pars` needs to be called afterward..
297    """
[4f2478e]298    with push_seed(seed):
299        # Note: the sort guarantees order `of calls to random number generator
[dd7fc12]300        random_pars = dict((p, _randomize_one(model_info, p, v))
301                           for p, v in sorted(pars.items()))
302    return random_pars
[cd3dba0]303
[17bbadd]304def constrain_pars(model_info, pars):
[dd7fc12]305    # type: (ModelInfo, ParameterSet) -> None
[9a66e65]306    """
307    Restrict parameters to valid values.
[caeb06d]308
309    This includes model specific code for models such as capped_cylinder
310    which need to support within model constraints (cap radius more than
311    cylinder radius in this case).
[dd7fc12]312
313    Warning: this updates the *pars* dictionary in place.
[9a66e65]314    """
[6d6508e]315    name = model_info.id
[17bbadd]316    # if it is a product model, then just look at the form factor since
317    # none of the structure factors need any constraints.
318    if '*' in name:
319        name = name.split('*')[0]
320
[bb2d187]321    if name == 'capped_cylinder' and pars['radius_cap'] < pars['radius']:
322        pars['radius'], pars['radius_cap'] = pars['radius_cap'], pars['radius']
323    if name == 'barbell' and pars['radius_bell'] < pars['radius']:
324        pars['radius'], pars['radius_bell'] = pars['radius_bell'], pars['radius']
[b514adf]325
326    # Limit guinier to an Rg such that Iq > 1e-30 (single precision cutoff)
327    if name == 'guinier':
328        #q_max = 0.2  # mid q maximum
329        q_max = 1.0  # high q maximum
330        rg_max = np.sqrt(90*np.log(10) + 3*np.log(pars['scale']))/q_max
[caeb06d]331        pars['rg'] = min(pars['rg'], rg_max)
[cd3dba0]332
[82c299f]333    if name == 'rpa':
334        # Make sure phi sums to 1.0
335        if pars['case_num'] < 2:
[8bd7b77]336            pars['Phi1'] = 0.
337            pars['Phi2'] = 0.
[82c299f]338        elif pars['case_num'] < 5:
[8bd7b77]339            pars['Phi1'] = 0.
340        total = sum(pars['Phi'+c] for c in '1234')
341        for c in '1234':
[82c299f]342            pars['Phi'+c] /= total
343
[d6850fa]344def parlist(model_info, pars, is2d):
[dd7fc12]345    # type: (ModelInfo, ParameterSet, bool) -> str
[caeb06d]346    """
347    Format the parameter list for printing.
348    """
[a4a7308]349    lines = []
[6d6508e]350    parameters = model_info.parameters
[d19962c]351    for p in parameters.user_parameters(pars, is2d):
352        fields = dict(
353            value=pars.get(p.id, p.default),
354            pd=pars.get(p.id+"_pd", 0.),
355            n=int(pars.get(p.id+"_pd_n", 0)),
356            nsigma=pars.get(p.id+"_pd_nsgima", 3.),
[dd7fc12]357            pdtype=pars.get(p.id+"_pd_type", 'gaussian'),
[bd49c79]358            relative_pd=p.relative_pd,
[dd7fc12]359        )
[d19962c]360        lines.append(_format_par(p.name, **fields))
[a4a7308]361    return "\n".join(lines)
362
363    #return "\n".join("%s: %s"%(p, v) for p, v in sorted(pars.items()))
364
[bd49c79]365def _format_par(name, value=0., pd=0., n=0, nsigma=3., pdtype='gaussian',
366                relative_pd=False):
[dd7fc12]367    # type: (str, float, float, int, float, str) -> str
[a4a7308]368    line = "%s: %g"%(name, value)
369    if pd != 0.  and n != 0:
[bd49c79]370        if relative_pd:
371            pd *= value
[a4a7308]372        line += " +/- %g  (%d points in [-%g,%g] sigma %s)"\
[dd7fc12]373                % (pd, n, nsigma, nsigma, pdtype)
[a4a7308]374    return line
[87985ca]375
376def suppress_pd(pars):
[dd7fc12]377    # type: (ParameterSet) -> ParameterSet
[87985ca]378    """
379    Suppress theta_pd for now until the normalization is resolved.
380
381    May also suppress complete polydispersity of the model to test
382    models more quickly.
383    """
[f4f3919]384    pars = pars.copy()
[87985ca]385    for p in pars:
[8b25ee1]386        if p.endswith("_pd_n"): pars[p] = 0
[f4f3919]387    return pars
[87985ca]388
[17bbadd]389def eval_sasview(model_info, data):
[dd7fc12]390    # type: (Modelinfo, Data) -> Calculator
[caeb06d]391    """
[f247314]392    Return a model calculator using the pre-4.0 SasView models.
[caeb06d]393    """
[dc056b9]394    # importing sas here so that the error message will be that sas failed to
395    # import rather than the more obscure smear_selection not imported error
[2bebe2b]396    import sas
[dd7fc12]397    import sas.models
[8d62008]398    from sas.models.qsmearing import smear_selection
399    from sas.models.MultiplicationModel import MultiplicationModel
[050c2c8]400    from sas.models.dispersion_models import models as dispersers
[ec7e360]401
[256dfe1]402    def get_model_class(name):
[dd7fc12]403        # type: (str) -> "sas.models.BaseComponent"
[17bbadd]404        #print("new",sorted(_pars.items()))
[dd7fc12]405        __import__('sas.models.' + name)
[17bbadd]406        ModelClass = getattr(getattr(sas.models, name, None), name, None)
407        if ModelClass is None:
408            raise ValueError("could not find model %r in sas.models"%name)
[256dfe1]409        return ModelClass
410
411    # WARNING: ugly hack when handling model!
412    # Sasview models with multiplicity need to be created with the target
413    # multiplicity, so we cannot create the target model ahead of time for
414    # for multiplicity models.  Instead we store the model in a list and
415    # update the first element of that list with the new multiplicity model
416    # every time we evaluate.
[17bbadd]417
418    # grab the sasview model, or create it if it is a product model
[6d6508e]419    if model_info.composition:
420        composition_type, parts = model_info.composition
[17bbadd]421        if composition_type == 'product':
[51ec7e8]422            P, S = [get_model_class(revert_name(p))() for p in parts]
[256dfe1]423            model = [MultiplicationModel(P, S)]
[17bbadd]424        else:
[72a081d]425            raise ValueError("sasview mixture models not supported by compare")
[17bbadd]426    else:
[f3bd37f]427        old_name = revert_name(model_info)
428        if old_name is None:
429            raise ValueError("model %r does not exist in old sasview"
430                            % model_info.id)
[256dfe1]431        ModelClass = get_model_class(old_name)
432        model = [ModelClass()]
[050c2c8]433    model[0].disperser_handles = {}
[216a9e1]434
[17bbadd]435    # build a smearer with which to call the model, if necessary
436    smearer = smear_selection(data, model=model)
[ec7e360]437    if hasattr(data, 'qx_data'):
438        q = np.sqrt(data.qx_data**2 + data.qy_data**2)
439        index = ((~data.mask) & (~np.isnan(data.data))
440                 & (q >= data.qmin) & (q <= data.qmax))
441        if smearer is not None:
442            smearer.model = model  # because smear_selection has a bug
443            smearer.accuracy = data.accuracy
444            smearer.set_index(index)
[256dfe1]445            def _call_smearer():
446                smearer.model = model[0]
447                return smearer.get_value()
[b32dafd]448            theory = _call_smearer
[ec7e360]449        else:
[256dfe1]450            theory = lambda: model[0].evalDistribution([data.qx_data[index],
451                                                        data.qy_data[index]])
[ec7e360]452    elif smearer is not None:
[256dfe1]453        theory = lambda: smearer(model[0].evalDistribution(data.x))
[ec7e360]454    else:
[256dfe1]455        theory = lambda: model[0].evalDistribution(data.x)
[ec7e360]456
457    def calculator(**pars):
[dd7fc12]458        # type: (float, ...) -> np.ndarray
[caeb06d]459        """
460        Sasview calculator for model.
461        """
[256dfe1]462        oldpars = revert_pars(model_info, pars)
[bd49c79]463        # For multiplicity models, create a model with the correct multiplicity
464        control = oldpars.pop("CONTROL", None)
465        if control is not None:
466            # sphericalSLD has one fewer multiplicity.  This update should
467            # happen in revert_pars, but it hasn't been called yet.
468            model[0] = ModelClass(control)
469        # paying for parameter conversion each time to keep life simple, if not fast
[050c2c8]470        for k, v in oldpars.items():
471            if k.endswith('.type'):
472                par = k[:-5]
[6831fa0]473                if v == 'gaussian': continue
[050c2c8]474                cls = dispersers[v if v != 'rectangle' else 'rectangula']
475                handle = cls()
476                model[0].disperser_handles[par] = handle
[6831fa0]477                try:
478                    model[0].set_dispersion(par, handle)
479                except Exception:
480                    exception.annotate_exception("while setting %s to %r"
481                                                 %(par, v))
482                    raise
483
[050c2c8]484
[f67f26c]485        #print("sasview pars",oldpars)
[256dfe1]486        for k, v in oldpars.items():
[dd7fc12]487            name_attr = k.split('.')  # polydispersity components
488            if len(name_attr) == 2:
[050c2c8]489                par, disp_par = name_attr
490                model[0].dispersion[par][disp_par] = v
[ec7e360]491            else:
[256dfe1]492                model[0].setParam(k, v)
[ec7e360]493        return theory()
494
495    calculator.engine = "sasview"
496    return calculator
497
498DTYPE_MAP = {
499    'half': '16',
500    'fast': 'fast',
501    'single': '32',
502    'double': '64',
503    'quad': '128',
504    'f16': '16',
505    'f32': '32',
506    'f64': '64',
507    'longdouble': '128',
508}
[17bbadd]509def eval_opencl(model_info, data, dtype='single', cutoff=0.):
[dd7fc12]510    # type: (ModelInfo, Data, str, float) -> Calculator
[caeb06d]511    """
512    Return a model calculator using the OpenCL calculation engine.
513    """
[a738209]514    if not core.HAVE_OPENCL:
515        raise RuntimeError("OpenCL not available")
516    model = core.build_model(model_info, dtype=dtype, platform="ocl")
[7cf2cfd]517    calculator = DirectModel(data, model, cutoff=cutoff)
[ec7e360]518    calculator.engine = "OCL%s"%DTYPE_MAP[dtype]
519    return calculator
[216a9e1]520
[17bbadd]521def eval_ctypes(model_info, data, dtype='double', cutoff=0.):
[dd7fc12]522    # type: (ModelInfo, Data, str, float) -> Calculator
[9cfcac8]523    """
524    Return a model calculator using the DLL calculation engine.
525    """
[72a081d]526    model = core.build_model(model_info, dtype=dtype, platform="dll")
[7cf2cfd]527    calculator = DirectModel(data, model, cutoff=cutoff)
[ec7e360]528    calculator.engine = "OMP%s"%DTYPE_MAP[dtype]
529    return calculator
530
[b32dafd]531def time_calculation(calculator, pars, evals=1):
[dd7fc12]532    # type: (Calculator, ParameterSet, int) -> Tuple[np.ndarray, float]
[caeb06d]533    """
534    Compute the average calculation time over N evaluations.
535
536    An additional call is generated without polydispersity in order to
537    initialize the calculation engine, and make the average more stable.
538    """
[ec7e360]539    # initialize the code so time is more accurate
[b32dafd]540    if evals > 1:
[dd7fc12]541        calculator(**suppress_pd(pars))
[216a9e1]542    toc = tic()
[dd7fc12]543    # make sure there is at least one eval
544    value = calculator(**pars)
[b32dafd]545    for _ in range(evals-1):
[7cf2cfd]546        value = calculator(**pars)
[b32dafd]547    average_time = toc()*1000. / evals
[f2f67a6]548    #print("I(q)",value)
[216a9e1]549    return value, average_time
550
[ec7e360]551def make_data(opts):
[dd7fc12]552    # type: (Dict[str, Any]) -> Tuple[Data, np.ndarray]
[caeb06d]553    """
554    Generate an empty dataset, used with the model to set Q points
555    and resolution.
556
557    *opts* contains the options, with 'qmax', 'nq', 'res',
558    'accuracy', 'is2d' and 'view' parsed from the command line.
559    """
[ec7e360]560    qmax, nq, res = opts['qmax'], opts['nq'], opts['res']
561    if opts['is2d']:
[dd7fc12]562        q = np.linspace(-qmax, qmax, nq)  # type: np.ndarray
563        data = empty_data2D(q, resolution=res)
[ec7e360]564        data.accuracy = opts['accuracy']
[ea75043]565        set_beam_stop(data, 0.0004)
[87985ca]566        index = ~data.mask
[216a9e1]567    else:
[e78edc4]568        if opts['view'] == 'log' and not opts['zero']:
[b89f519]569            qmax = math.log10(qmax)
[ec7e360]570            q = np.logspace(qmax-3, qmax, nq)
[b89f519]571        else:
[ec7e360]572            q = np.linspace(0.001*qmax, qmax, nq)
[e78edc4]573        if opts['zero']:
574            q = np.hstack((0, q))
[ec7e360]575        data = empty_data1D(q, resolution=res)
[216a9e1]576        index = slice(None, None)
577    return data, index
578
[17bbadd]579def make_engine(model_info, data, dtype, cutoff):
[dd7fc12]580    # type: (ModelInfo, Data, str, float) -> Calculator
[caeb06d]581    """
582    Generate the appropriate calculation engine for the given datatype.
583
584    Datatypes with '!' appended are evaluated using external C DLLs rather
585    than OpenCL.
586    """
[ec7e360]587    if dtype == 'sasview':
[17bbadd]588        return eval_sasview(model_info, data)
[ec7e360]589    elif dtype.endswith('!'):
[17bbadd]590        return eval_ctypes(model_info, data, dtype=dtype[:-1], cutoff=cutoff)
[ec7e360]591    else:
[17bbadd]592        return eval_opencl(model_info, data, dtype=dtype, cutoff=cutoff)
[87985ca]593
[e78edc4]594def _show_invalid(data, theory):
[dd7fc12]595    # type: (Data, np.ma.ndarray) -> None
596    """
597    Display a list of the non-finite values in theory.
598    """
[e78edc4]599    if not theory.mask.any():
600        return
601
602    if hasattr(data, 'x'):
603        bad = zip(data.x[theory.mask], theory[theory.mask])
[dd7fc12]604        print("   *** ", ", ".join("I(%g)=%g"%(x, y) for x, y in bad))
[e78edc4]605
606
[013adb7]607def compare(opts, limits=None):
[dd7fc12]608    # type: (Dict[str, Any], Optional[Tuple[float, float]]) -> Tuple[float, float]
[caeb06d]609    """
610    Preform a comparison using options from the command line.
611
612    *limits* are the limits on the values to use, either to set the y-axis
613    for 1D or to set the colormap scale for 2D.  If None, then they are
614    inferred from the data and returned. When exploring using Bumps,
615    the limits are set when the model is initially called, and maintained
616    as the values are adjusted, making it easier to see the effects of the
617    parameters.
618    """
[ff1fff5]619    n_base, n_comp = opts['count']
620    pars, pars2 = opts['pars']
[ec7e360]621    data = opts['data']
[87985ca]622
[dd7fc12]623    # silence the linter
[b32dafd]624    base = opts['engines'][0] if n_base else None
625    comp = opts['engines'][1] if n_comp else None
[dd7fc12]626    base_time = comp_time = None
627    base_value = comp_value = resid = relerr = None
628
[4b41184]629    # Base calculation
[b32dafd]630    if n_base > 0:
[319ab14]631        try:
[b32dafd]632            base_raw, base_time = time_calculation(base, pars, n_base)
[dd7fc12]633            base_value = np.ma.masked_invalid(base_raw)
[af92b73]634            print("%s t=%.2f ms, intensity=%.0f"
[e78edc4]635                  % (base.engine, base_time, base_value.sum()))
636            _show_invalid(data, base_value)
[319ab14]637        except ImportError:
638            traceback.print_exc()
[b32dafd]639            n_base = 0
[4b41184]640
641    # Comparison calculation
[b32dafd]642    if n_comp > 0:
[7cf2cfd]643        try:
[ff1fff5]644            comp_raw, comp_time = time_calculation(comp, pars2, n_comp)
[dd7fc12]645            comp_value = np.ma.masked_invalid(comp_raw)
[af92b73]646            print("%s t=%.2f ms, intensity=%.0f"
[e78edc4]647                  % (comp.engine, comp_time, comp_value.sum()))
648            _show_invalid(data, comp_value)
[7cf2cfd]649        except ImportError:
[5753e4e]650            traceback.print_exc()
[b32dafd]651            n_comp = 0
[87985ca]652
653    # Compare, but only if computing both forms
[b32dafd]654    if n_base > 0 and n_comp > 0:
[ec7e360]655        resid = (base_value - comp_value)
[b32dafd]656        relerr = resid/np.where(comp_value != 0., abs(comp_value), 1.0)
[d15a908]657        _print_stats("|%s-%s|"
658                     % (base.engine, comp.engine) + (" "*(3+len(comp.engine))),
[caeb06d]659                     resid)
[d15a908]660        _print_stats("|(%s-%s)/%s|"
661                     % (base.engine, comp.engine, comp.engine),
[caeb06d]662                     relerr)
[87985ca]663
664    # Plot if requested
[ec7e360]665    if not opts['plot'] and not opts['explore']: return
666    view = opts['view']
[1726b21]667    import matplotlib.pyplot as plt
[013adb7]668    if limits is None:
669        vmin, vmax = np.Inf, -np.Inf
[b32dafd]670        if n_base > 0:
[e78edc4]671            vmin = min(vmin, base_value.min())
672            vmax = max(vmax, base_value.max())
[b32dafd]673        if n_comp > 0:
[e78edc4]674            vmin = min(vmin, comp_value.min())
675            vmax = max(vmax, comp_value.max())
[013adb7]676        limits = vmin, vmax
677
[b32dafd]678    if n_base > 0:
679        if n_comp > 0: plt.subplot(131)
[841753c]680        plot_theory(data, base_value, view=view, use_data=False, limits=limits)
[af92b73]681        plt.title("%s t=%.2f ms"%(base.engine, base_time))
[ec7e360]682        #cbar_title = "log I"
[b32dafd]683    if n_comp > 0:
684        if n_base > 0: plt.subplot(132)
[841753c]685        plot_theory(data, comp_value, view=view, use_data=False, limits=limits)
[af92b73]686        plt.title("%s t=%.2f ms"%(comp.engine, comp_time))
[7cf2cfd]687        #cbar_title = "log I"
[b32dafd]688    if n_comp > 0 and n_base > 0:
[87985ca]689        plt.subplot(133)
[d5e650d]690        if not opts['rel_err']:
[caeb06d]691            err, errstr, errview = resid, "abs err", "linear"
[29f5536]692        else:
[caeb06d]693            err, errstr, errview = abs(relerr), "rel err", "log"
[9068f4c]694        #sorted = np.sort(err.flatten())
695        #cutoff = sorted[int(sorted.size*0.95)]
696        #err[err>cutoff] = cutoff
[4b41184]697        #err,errstr = base/comp,"ratio"
[841753c]698        plot_theory(data, None, resid=err, view=errview, use_data=False)
[d5e650d]699        if view == 'linear':
700            plt.xscale('linear')
[e78edc4]701        plt.title("max %s = %.3g"%(errstr, abs(err).max()))
[7cf2cfd]702        #cbar_title = errstr if errview=="linear" else "log "+errstr
703    #if is2D:
704    #    h = plt.colorbar()
705    #    h.ax.set_title(cbar_title)
[0c24a82]706    fig = plt.gcf()
[a0d75ce]707    extra_title = ' '+opts['title'] if opts['title'] else ''
[ff1fff5]708    fig.suptitle(":".join(opts['name']) + extra_title)
[ba69383]709
[9068f4c]710    if n_comp > 0 and n_base > 0 and opts['show_hist']:
[ba69383]711        plt.figure()
[346bc88]712        v = relerr
[caeb06d]713        v[v == 0] = 0.5*np.min(np.abs(v[v != 0]))
714        plt.hist(np.log10(np.abs(v)), normed=1, bins=50)
715        plt.xlabel('log10(err), err = |(%s - %s) / %s|'
716                   % (base.engine, comp.engine, comp.engine))
[ba69383]717        plt.ylabel('P(err)')
[ec7e360]718        plt.title('Distribution of relative error between calculation engines')
[ba69383]719
[ec7e360]720    if not opts['explore']:
721        plt.show()
[8a20be5]722
[013adb7]723    return limits
724
[0763009]725def _print_stats(label, err):
[dd7fc12]726    # type: (str, np.ma.ndarray) -> None
727    # work with trimmed data, not the full set
[e78edc4]728    sorted_err = np.sort(abs(err.compressed()))
[dd7fc12]729    p50 = int((len(sorted_err)-1)*0.50)
730    p98 = int((len(sorted_err)-1)*0.98)
[0763009]731    data = [
732        "max:%.3e"%sorted_err[-1],
733        "median:%.3e"%sorted_err[p50],
734        "98%%:%.3e"%sorted_err[p98],
[dd7fc12]735        "rms:%.3e"%np.sqrt(np.mean(sorted_err**2)),
736        "zero-offset:%+.3e"%np.mean(sorted_err),
[0763009]737        ]
[caeb06d]738    print(label+"  "+"  ".join(data))
[0763009]739
740
741
[87985ca]742# ===========================================================================
743#
[216a9e1]744NAME_OPTIONS = set([
[5d316e9]745    'plot', 'noplot',
[ec7e360]746    'half', 'fast', 'single', 'double',
747    'single!', 'double!', 'quad!', 'sasview',
[e78edc4]748    'lowq', 'midq', 'highq', 'exq', 'zero',
[5d316e9]749    '2d', '1d',
750    'preset', 'random',
751    'poly', 'mono',
752    'nopars', 'pars',
753    'rel', 'abs',
[b89f519]754    'linear', 'log', 'q4',
[5d316e9]755    'hist', 'nohist',
[234c532]756    'edit', 'html',
[98d6cfc]757    'demo', 'default',
[216a9e1]758    ])
759VALUE_OPTIONS = [
760    # Note: random is both a name option and a value option
[a0d75ce]761    'cutoff', 'random', 'nq', 'res', 'accuracy', 'title',
[87985ca]762    ]
763
[b32dafd]764def columnize(items, indent="", width=79):
[dd7fc12]765    # type: (List[str], str, int) -> str
[caeb06d]766    """
[1d4017a]767    Format a list of strings into columns.
768
769    Returns a string with carriage returns ready for printing.
[caeb06d]770    """
[b32dafd]771    column_width = max(len(w) for w in items) + 1
[7cf2cfd]772    num_columns = (width - len(indent)) // column_width
[b32dafd]773    num_rows = len(items) // num_columns
774    items = items + [""] * (num_rows * num_columns - len(items))
775    columns = [items[k*num_rows:(k+1)*num_rows] for k in range(num_columns)]
[7cf2cfd]776    lines = [" ".join("%-*s"%(column_width, entry) for entry in row)
777             for row in zip(*columns)]
778    output = indent + ("\n"+indent).join(lines)
779    return output
780
781
[98d6cfc]782def get_pars(model_info, use_demo=False):
[dd7fc12]783    # type: (ModelInfo, bool) -> ParameterSet
[caeb06d]784    """
785    Extract demo parameters from the model definition.
786    """
[ec7e360]787    # Get the default values for the parameters
[c499331]788    pars = {}
[6d6508e]789    for p in model_info.parameters.call_parameters:
[c499331]790        parts = [('', p.default)]
791        if p.polydisperse:
792            parts.append(('_pd', 0.0))
793            parts.append(('_pd_n', 0))
794            parts.append(('_pd_nsigma', 3.0))
795            parts.append(('_pd_type', "gaussian"))
796        for ext, val in parts:
797            if p.length > 1:
[b32dafd]798                dict(("%s%d%s" % (p.id, k, ext), val)
799                     for k in range(1, p.length+1))
[c499331]800            else:
[b32dafd]801                pars[p.id + ext] = val
[ec7e360]802
803    # Plug in values given in demo
[98d6cfc]804    if use_demo:
[6d6508e]805        pars.update(model_info.demo)
[373d1b6]806    return pars
807
[ff1fff5]808INTEGER_RE = re.compile("^[+-]?[1-9][0-9]*$")
809def isnumber(str):
810    match = FLOAT_RE.match(str)
811    isfloat = (match and not str[match.end():])
812    return isfloat or INTEGER_RE.match(str)
[17bbadd]813
[424fe00]814def parse_opts(argv):
815    # type: (List[str]) -> Dict[str, Any]
[caeb06d]816    """
817    Parse command line options.
818    """
[fc0fcd0]819    MODELS = core.list_models()
[424fe00]820    flags = [arg for arg in argv
[caeb06d]821             if arg.startswith('-')]
[424fe00]822    values = [arg for arg in argv
[caeb06d]823              if not arg.startswith('-') and '=' in arg]
[424fe00]824    positional_args = [arg for arg in argv
[caeb06d]825            if not arg.startswith('-') and '=' not in arg]
[d547f16]826    models = "\n    ".join("%-15s"%v for v in MODELS)
[424fe00]827    if len(positional_args) == 0:
[7cf2cfd]828        print(USAGE)
[caeb06d]829        print("\nAvailable models:")
[7cf2cfd]830        print(columnize(MODELS, indent="  "))
[424fe00]831        return None
832    if len(positional_args) > 3:
[9cfcac8]833        print("expected parameters: model N1 N2")
[87985ca]834
[ec7e360]835    invalid = [o[1:] for o in flags
[216a9e1]836               if o[1:] not in NAME_OPTIONS
[d15a908]837               and not any(o.startswith('-%s='%t) for t in VALUE_OPTIONS)]
[87985ca]838    if invalid:
[9404dd3]839        print("Invalid options: %s"%(", ".join(invalid)))
[424fe00]840        return None
[87985ca]841
[ff1fff5]842    name = positional_args[0]
843    n1 = int(positional_args[1]) if len(positional_args) > 1 else 1
844    n2 = int(positional_args[2]) if len(positional_args) > 2 else 1
[ec7e360]845
[d15a908]846    # pylint: disable=bad-whitespace
[ec7e360]847    # Interpret the flags
848    opts = {
849        'plot'      : True,
850        'view'      : 'log',
851        'is2d'      : False,
852        'qmax'      : 0.05,
853        'nq'        : 128,
854        'res'       : 0.0,
855        'accuracy'  : 'Low',
[72a081d]856        'cutoff'    : 0.0,
[ec7e360]857        'seed'      : -1,  # default to preset
858        'mono'      : False,
859        'show_pars' : False,
860        'show_hist' : False,
861        'rel_err'   : True,
862        'explore'   : False,
[98d6cfc]863        'use_demo'  : True,
[dd7fc12]864        'zero'      : False,
[234c532]865        'html'      : False,
[a0d75ce]866        'title'     : None,
[ec7e360]867    }
868    engines = []
869    for arg in flags:
870        if arg == '-noplot':    opts['plot'] = False
871        elif arg == '-plot':    opts['plot'] = True
872        elif arg == '-linear':  opts['view'] = 'linear'
873        elif arg == '-log':     opts['view'] = 'log'
874        elif arg == '-q4':      opts['view'] = 'q4'
875        elif arg == '-1d':      opts['is2d'] = False
876        elif arg == '-2d':      opts['is2d'] = True
877        elif arg == '-exq':     opts['qmax'] = 10.0
878        elif arg == '-highq':   opts['qmax'] = 1.0
879        elif arg == '-midq':    opts['qmax'] = 0.2
[ce0b154]880        elif arg == '-lowq':    opts['qmax'] = 0.05
[e78edc4]881        elif arg == '-zero':    opts['zero'] = True
[ec7e360]882        elif arg.startswith('-nq='):       opts['nq'] = int(arg[4:])
883        elif arg.startswith('-res='):      opts['res'] = float(arg[5:])
884        elif arg.startswith('-accuracy='): opts['accuracy'] = arg[10:]
885        elif arg.startswith('-cutoff='):   opts['cutoff'] = float(arg[8:])
886        elif arg.startswith('-random='):   opts['seed'] = int(arg[8:])
[a0d75ce]887        elif arg.startswith('-title'):     opts['title'] = arg[7:]
[dd7fc12]888        elif arg == '-random':  opts['seed'] = np.random.randint(1000000)
[ec7e360]889        elif arg == '-preset':  opts['seed'] = -1
890        elif arg == '-mono':    opts['mono'] = True
891        elif arg == '-poly':    opts['mono'] = False
892        elif arg == '-pars':    opts['show_pars'] = True
893        elif arg == '-nopars':  opts['show_pars'] = False
894        elif arg == '-hist':    opts['show_hist'] = True
895        elif arg == '-nohist':  opts['show_hist'] = False
896        elif arg == '-rel':     opts['rel_err'] = True
897        elif arg == '-abs':     opts['rel_err'] = False
898        elif arg == '-half':    engines.append(arg[1:])
899        elif arg == '-fast':    engines.append(arg[1:])
900        elif arg == '-single':  engines.append(arg[1:])
901        elif arg == '-double':  engines.append(arg[1:])
902        elif arg == '-single!': engines.append(arg[1:])
903        elif arg == '-double!': engines.append(arg[1:])
904        elif arg == '-quad!':   engines.append(arg[1:])
905        elif arg == '-sasview': engines.append(arg[1:])
906        elif arg == '-edit':    opts['explore'] = True
[98d6cfc]907        elif arg == '-demo':    opts['use_demo'] = True
908        elif arg == '-default':    opts['use_demo'] = False
[234c532]909        elif arg == '-html':    opts['html'] = True
[d15a908]910    # pylint: enable=bad-whitespace
[ec7e360]911
[ff1fff5]912    if ':' in name:
913        name, name2 = name.split(':',2)
914    else:
915        name2 = name
916    try:
917        model_info = core.load_model_info(name)
918        model_info2 = core.load_model_info(name2) if name2 != name else model_info
919    except ImportError as exc:
920        print(str(exc))
921        print("Could not find model; use one of:\n    " + models)
922        return None
[87985ca]923
[ec7e360]924    # Get demo parameters from model definition, or use default parameters
925    # if model does not define demo parameters
[98d6cfc]926    pars = get_pars(model_info, opts['use_demo'])
[ff1fff5]927    pars2 = get_pars(model_info2, opts['use_demo'])
[248561a]928    pars2.update((k, v) for k, v in pars.items() if k in pars2)
[ff1fff5]929    # randomize parameters
930    #pars.update(set_pars)  # set value before random to control range
931    if opts['seed'] > -1:
932        pars = randomize_pars(model_info, pars, seed=opts['seed'])
933        if model_info != model_info2:
934            pars2 = randomize_pars(model_info2, pars2, seed=opts['seed'])
935        else:
936            pars2 = pars.copy()
937        print("Randomize using -random=%i"%opts['seed'])
938    if opts['mono']:
939        pars = suppress_pd(pars)
940        pars2 = suppress_pd(pars2)
[87985ca]941
942    # Fill in parameters given on the command line
[ec7e360]943    presets = {}
[ff1fff5]944    presets2 = {}
[ec7e360]945    for arg in values:
[d15a908]946        k, v = arg.split('=', 1)
[ff1fff5]947        if k not in pars and k not in pars2:
[ec7e360]948            # extract base name without polydispersity info
[87985ca]949            s = set(p.split('_pd')[0] for p in pars)
[d15a908]950            print("%r invalid; parameters are: %s"%(k, ", ".join(sorted(s))))
[424fe00]951            return None
[ff1fff5]952        v1, v2 = v.split(':',2) if ':' in v else (v,v)
953        if v1 and k in pars:
954            presets[k] = float(v1) if isnumber(v1) else v1
955        if v2 and k in pars2:
956            presets2[k] = float(v2) if isnumber(v2) else v2
957
958    # Evaluate preset parameter expressions
[248561a]959    context = MATH.copy()
960    context.update(pars)
[ff1fff5]961    context.update((k,v) for k,v in presets.items() if isinstance(v, float))
962    for k, v in presets.items():
963        if not isinstance(v, float) and not k.endswith('_type'):
964            presets[k] = eval(v, context)
965    context.update(presets)
966    context.update((k,v) for k,v in presets2.items() if isinstance(v, float))
967    for k, v in presets2.items():
968        if not isinstance(v, float) and not k.endswith('_type'):
969            presets2[k] = eval(v, context)
970
971    # update parameters with presets
[ec7e360]972    pars.update(presets)  # set value after random to control value
[ff1fff5]973    pars2.update(presets2)  # set value after random to control value
[fcd7bbd]974    #import pprint; pprint.pprint(model_info)
[17bbadd]975    constrain_pars(model_info, pars)
[ff1fff5]976    constrain_pars(model_info2, pars2)
977
978    same_model = name == name2 and pars == pars
979    if len(engines) == 0:
980        if same_model:
981            engines.extend(['single', 'double'])
982        else:
983            engines.extend(['single', 'single'])
984    elif len(engines) == 1:
985        if not same_model:
986            engines.append(engines[0])
987        elif engines[0] == 'double':
988            engines.append('single')
989        else:
990            engines.append('double')
991    elif len(engines) > 2:
992        del engines[2:]
993
994    use_sasview = any(engine == 'sasview' and count > 0
995                      for engine, count in zip(engines, [n1, n2]))
[fa1582e]996    if use_sasview:
997        constrain_new_to_old(model_info, pars)
[ff1fff5]998        constrain_new_to_old(model_info2, pars2)
999
[ec7e360]1000    if opts['show_pars']:
[248561a]1001        if not same_model:
1002            print("==== %s ====="%model_info.name)
1003            print(str(parlist(model_info, pars, opts['is2d'])))
1004            print("==== %s ====="%model_info2.name)
1005            print(str(parlist(model_info2, pars2, opts['is2d'])))
1006        else:
1007            print(str(parlist(model_info, pars, opts['is2d'])))
[ec7e360]1008
1009    # Create the computational engines
[d15a908]1010    data, _ = make_data(opts)
[9cfcac8]1011    if n1:
[17bbadd]1012        base = make_engine(model_info, data, engines[0], opts['cutoff'])
[ec7e360]1013    else:
1014        base = None
[9cfcac8]1015    if n2:
[ff1fff5]1016        comp = make_engine(model_info2, data, engines[1], opts['cutoff'])
[ec7e360]1017    else:
1018        comp = None
1019
[d15a908]1020    # pylint: disable=bad-whitespace
[ec7e360]1021    # Remember it all
1022    opts.update({
1023        'data'      : data,
[ff1fff5]1024        'name'      : [name, name2],
1025        'def'       : [model_info, model_info2],
1026        'count'     : [n1, n2],
1027        'presets'   : [presets, presets2],
1028        'pars'      : [pars, pars2],
[ec7e360]1029        'engines'   : [base, comp],
1030    })
[d15a908]1031    # pylint: enable=bad-whitespace
[ec7e360]1032
1033    return opts
1034
[234c532]1035def show_docs(opts):
1036    # type: (Dict[str, Any]) -> None
1037    """
1038    show html docs for the model
1039    """
1040    import wx  # type: ignore
1041    from .generate import view_html_from_info
1042    app = wx.App() if wx.GetApp() is None else None
[ff1fff5]1043    view_html_from_info(opts['def'][0])
[234c532]1044    if app: app.MainLoop()
1045
1046
[ec7e360]1047def explore(opts):
[dd7fc12]1048    # type: (Dict[str, Any]) -> None
[d15a908]1049    """
[234c532]1050    explore the model using the bumps gui.
[d15a908]1051    """
[7ae2b7f]1052    import wx  # type: ignore
1053    from bumps.names import FitProblem  # type: ignore
1054    from bumps.gui.app_frame import AppFrame  # type: ignore
[ec7e360]1055
[d15a908]1056    is_mac = "cocoa" in wx.version()
[80013a6]1057    # Create an app if not running embedded
1058    app = wx.App() if wx.GetApp() is None else None
1059    problem = FitProblem(Explore(opts))
1060    frame = AppFrame(parent=None, title="explore", size=(1000,700))
[d15a908]1061    if not is_mac: frame.Show()
[ec7e360]1062    frame.panel.set_model(model=problem)
1063    frame.panel.Layout()
1064    frame.panel.aui.Split(0, wx.TOP)
[d15a908]1065    if is_mac: frame.Show()
[80013a6]1066    # If running withing an app, start the main loop
1067    if app: app.MainLoop()
[ec7e360]1068
1069class Explore(object):
1070    """
[d15a908]1071    Bumps wrapper for a SAS model comparison.
1072
1073    The resulting object can be used as a Bumps fit problem so that
1074    parameters can be adjusted in the GUI, with plots updated on the fly.
[ec7e360]1075    """
1076    def __init__(self, opts):
[dd7fc12]1077        # type: (Dict[str, Any]) -> None
[7ae2b7f]1078        from bumps.cli import config_matplotlib  # type: ignore
[608e31e]1079        from . import bumps_model
[ec7e360]1080        config_matplotlib()
1081        self.opts = opts
[ff1fff5]1082        model_info = opts['def'][0]
1083        pars, pd_types = bumps_model.create_parameters(model_info, **opts['pars'][0])
[21b116f]1084        # Initialize parameter ranges, fixing the 2D parameters for 1D data.
[ec7e360]1085        if not opts['is2d']:
[6d6508e]1086            for p in model_info.parameters.user_parameters(is2d=False):
[303d8d6]1087                for ext in ['', '_pd', '_pd_n', '_pd_nsigma']:
[69aa451]1088                    k = p.name+ext
[303d8d6]1089                    v = pars.get(k, None)
1090                    if v is not None:
1091                        v.range(*parameter_range(k, v.value))
[ec7e360]1092        else:
[013adb7]1093            for k, v in pars.items():
[ec7e360]1094                v.range(*parameter_range(k, v.value))
1095
1096        self.pars = pars
1097        self.pd_types = pd_types
[013adb7]1098        self.limits = None
[ec7e360]1099
1100    def numpoints(self):
[dd7fc12]1101        # type: () -> int
[ec7e360]1102        """
[608e31e]1103        Return the number of points.
[ec7e360]1104        """
1105        return len(self.pars) + 1  # so dof is 1
1106
1107    def parameters(self):
[dd7fc12]1108        # type: () -> Any   # Dict/List hierarchy of parameters
[ec7e360]1109        """
[608e31e]1110        Return a dictionary of parameters.
[ec7e360]1111        """
1112        return self.pars
1113
1114    def nllf(self):
[dd7fc12]1115        # type: () -> float
[608e31e]1116        """
1117        Return cost.
1118        """
[d15a908]1119        # pylint: disable=no-self-use
[ec7e360]1120        return 0.  # No nllf
1121
1122    def plot(self, view='log'):
[dd7fc12]1123        # type: (str) -> None
[ec7e360]1124        """
1125        Plot the data and residuals.
1126        """
[608e31e]1127        pars = dict((k, v.value) for k, v in self.pars.items())
[ec7e360]1128        pars.update(self.pd_types)
[ff1fff5]1129        self.opts['pars'][0] = pars
1130        self.opts['pars'][1] = pars
[013adb7]1131        limits = compare(self.opts, limits=self.limits)
1132        if self.limits is None:
1133            vmin, vmax = limits
[dd7fc12]1134            self.limits = vmax*1e-7, 1.3*vmax
[87985ca]1135
1136
[424fe00]1137def main(*argv):
1138    # type: (*str) -> None
[d15a908]1139    """
1140    Main program.
1141    """
[424fe00]1142    opts = parse_opts(argv)
1143    if opts is not None:
[234c532]1144        if opts['html']:
1145            show_docs(opts)
1146        elif opts['explore']:
[424fe00]1147            explore(opts)
1148        else:
1149            compare(opts)
[d15a908]1150
[8a20be5]1151if __name__ == "__main__":
[424fe00]1152    main(*sys.argv[1:])
Note: See TracBrowser for help on using the repository browser.