source: sasmodels/sasmodels/model_test.py @ 5fd684d

core_shell_microgelscostrafo411magnetic_modelrelease_v0.94release_v0.95ticket-1257-vesicle-productticket_1156ticket_1265_superballticket_822_more_unit_tests
Last change on this file since 5fd684d was 40a87fa, checked in by Paul Kienzle <pkienzle@…>, 8 years ago

lint and latex cleanup

  • Property mode set to 100644
File size: 11.4 KB
Line 
1# -*- coding: utf-8 -*-
2"""
3Run model unit tests.
4
5Usage::
6
7    python -m sasmodels.model_test [opencl|dll|opencl_and_dll] model1 model2 ...
8
9    if model1 is 'all', then all except the remaining models will be tested
10
11Each model is tested using the default parameters at q=0.1, (qx, qy)=(0.1, 0.1),
12and the ER and VR are computed.  The return values at these points are not
13considered.  The test is only to verify that the models run to completion,
14and do not produce inf or NaN.
15
16Tests are defined with the *tests* attribute in the model.py file.  *tests*
17is a list of individual tests to run, where each test consists of the
18parameter values for the test, the q-values and the expected results.  For
19the effective radius test, the q-value should be 'ER'.  For the VR test,
20the q-value should be 'VR'.  For 1-D tests, either specify the q value or
21a list of q-values, and the corresponding I(q) value, or list of I(q) values.
22
23That is::
24
25    tests = [
26        [ {parameters}, q, I(q)],
27        [ {parameters}, [q], [I(q)] ],
28        [ {parameters}, [q1, q2, ...], [I(q1), I(q2), ...]],
29
30        [ {parameters}, (qx, qy), I(qx, Iqy)],
31        [ {parameters}, [(qx1, qy1), (qx2, qy2), ...],
32                        [I(qx1, qy1), I(qx2, qy2), ...]],
33
34        [ {parameters}, 'ER', ER(pars) ],
35        [ {parameters}, 'VR', VR(pars) ],
36        ...
37    ]
38
39Parameters are *key:value* pairs, where key is one of the parameters of the
40model and value is the value to use for the test.  Any parameters not given
41in the parameter list will take on the default parameter value.
42
43Precision defaults to 5 digits (relative).
44"""
45from __future__ import print_function
46
47import sys
48import unittest
49
50import numpy as np  # type: ignore
51
52from .core import list_models, load_model_info, build_model, HAVE_OPENCL
53from .direct_model import call_kernel, call_ER, call_VR
54from .exception import annotate_exception
55from .modelinfo import expand_pars
56
57try:
58    from typing import List, Iterator, Callable
59except ImportError:
60    pass
61else:
62    from .modelinfo import ParameterTable, ParameterSet, TestCondition, ModelInfo
63    from .kernel import KernelModel
64
65
66def make_suite(loaders, models):
67    # type: (List[str], List[str]) -> unittest.TestSuite
68    """
69    Construct the pyunit test suite.
70
71    *loaders* is the list of kernel drivers to use, which is one of
72    *["dll", "opencl"]*, *["dll"]* or *["opencl"]*.  For python models,
73    the python driver is always used.
74
75    *models* is the list of models to test, or *["all"]* to test all models.
76    """
77    ModelTestCase = _hide_model_case_from_nose()
78    suite = unittest.TestSuite()
79
80    if models[0] == 'all':
81        skip = models[1:]
82        models = list_models()
83    else:
84        skip = []
85    for model_name in models:
86        if model_name in skip: continue
87        model_info = load_model_info(model_name)
88
89        #print('------')
90        #print('found tests in', model_name)
91        #print('------')
92
93        # if ispy then use the dll loader to call pykernel
94        # don't try to call cl kernel since it will not be
95        # available in some environmentes.
96        is_py = callable(model_info.Iq)
97
98        if is_py:  # kernel implemented in python
99            test_name = "Model: %s, Kernel: python"%model_name
100            test_method_name = "test_%s_python" % model_name
101            test = ModelTestCase(test_name, model_info,
102                                 test_method_name,
103                                 platform="dll",  # so that
104                                 dtype="double")
105            suite.addTest(test)
106        else:   # kernel implemented in C
107            # test using opencl if desired and available
108            if 'opencl' in loaders and HAVE_OPENCL:
109                test_name = "Model: %s, Kernel: OpenCL"%model_name
110                test_method_name = "test_%s_opencl" % model_name
111                # Using dtype=None so that the models that are only
112                # correct for double precision are not tested using
113                # single precision.  The choice is determined by the
114                # presence of *single=False* in the model file.
115                test = ModelTestCase(test_name, model_info,
116                                     test_method_name,
117                                     platform="ocl", dtype=None)
118                #print("defining", test_name)
119                suite.addTest(test)
120
121            # test using dll if desired
122            if 'dll' in loaders:
123                test_name = "Model: %s, Kernel: dll"%model_name
124                test_method_name = "test_%s_dll" % model_name
125                test = ModelTestCase(test_name, model_info,
126                                     test_method_name,
127                                     platform="dll",
128                                     dtype="double")
129                suite.addTest(test)
130
131    return suite
132
133
134def _hide_model_case_from_nose():
135    # type: () -> type
136    class ModelTestCase(unittest.TestCase):
137        """
138        Test suit for a particular model with a particular kernel driver.
139
140        The test suite runs a simple smoke test to make sure the model
141        functions, then runs the list of tests at the bottom of the model
142        description file.
143        """
144        def __init__(self, test_name, model_info, test_method_name,
145                     platform, dtype):
146            # type: (str, ModelInfo, str, str, DType) -> None
147            self.test_name = test_name
148            self.info = model_info
149            self.platform = platform
150            self.dtype = dtype
151
152            setattr(self, test_method_name, self.run_all)
153            unittest.TestCase.__init__(self, test_method_name)
154
155        def run_all(self):
156            # type: () -> None
157            """
158            Run all the tests in the test suite, including smoke tests.
159            """
160            smoke_tests = [
161                # test validity at reasonable values
162                ({}, 0.1, None),
163                ({}, (0.1, 0.1), None),
164                # test validity at q = 0
165                #({}, 0.0, None),
166                #({}, (0.0, 0.0), None),
167                # test vector form
168                ({}, [0.1]*2, [None]*2),
169                ({}, [(0.1, 0.1)]*2, [None]*2),
170                # test that ER/VR will run if they exist
171                ({}, 'ER', None),
172                ({}, 'VR', None),
173                ]
174
175            tests = self.info.tests
176            try:
177                model = build_model(self.info, dtype=self.dtype,
178                                    platform=self.platform)
179                for test in smoke_tests + tests:
180                    self.run_one(model, test)
181
182                if not tests and self.platform == "dll":
183                    ## Uncomment the following to make forgetting the test
184                    ## values an error.  Only do so for the "dll" tests
185                    ## to reduce noise from both opencl and dll, and because
186                    ## python kernels use platform="dll".
187                    #raise Exception("No test cases provided")
188                    pass
189
190            except:
191                annotate_exception(self.test_name)
192                raise
193
194        def run_one(self, model, test):
195            # type: (KernelModel, TestCondition) -> None
196            """Run a single test case."""
197            user_pars, x, y = test
198            pars = expand_pars(self.info.parameters, user_pars)
199
200            if not isinstance(y, list):
201                y = [y]
202            if not isinstance(x, list):
203                x = [x]
204
205            self.assertEqual(len(y), len(x))
206
207            if x[0] == 'ER':
208                actual = [call_ER(model.info, pars)]
209            elif x[0] == 'VR':
210                actual = [call_VR(model.info, pars)]
211            elif isinstance(x[0], tuple):
212                qx, qy = zip(*x)
213                q_vectors = [np.array(qx), np.array(qy)]
214                kernel = model.make_kernel(q_vectors)
215                actual = call_kernel(kernel, pars)
216            else:
217                q_vectors = [np.array(x)]
218                kernel = model.make_kernel(q_vectors)
219                actual = call_kernel(kernel, pars)
220
221            self.assertTrue(len(actual) > 0)
222            self.assertEqual(len(y), len(actual))
223
224            for xi, yi, actual_yi in zip(x, y, actual):
225                if yi is None:
226                    # smoke test --- make sure it runs and produces a value
227                    self.assertTrue(not np.isnan(actual_yi),
228                                    'invalid f(%s): %s' % (xi, actual_yi))
229                elif np.isnan(yi):
230                    self.assertTrue(np.isnan(actual_yi),
231                                    'f(%s): expected:%s; actual:%s'
232                                    % (xi, yi, actual_yi))
233                else:
234                    # is_near does not work for infinite values, so also test
235                    # for exact values.  Note that this will not
236                    self.assertTrue(yi == actual_yi or is_near(yi, actual_yi, 5),
237                                    'f(%s); expected:%s; actual:%s'
238                                    % (xi, yi, actual_yi))
239
240    return ModelTestCase
241
242def is_near(target, actual, digits=5):
243    # type: (float, float, int) -> bool
244    """
245    Returns true if *actual* is within *digits* significant digits of *target*.
246    """
247    import math
248    shift = 10**math.ceil(math.log10(abs(target)))
249    return abs(target-actual)/shift < 1.5*10**-digits
250
251def main():
252    # type: () -> int
253    """
254    Run tests given is sys.argv.
255
256    Returns 0 if success or 1 if any tests fail.
257    """
258    try:
259        from xmlrunner import XMLTestRunner as TestRunner
260        test_args = {'output': 'logs'}
261    except ImportError:
262        from unittest import TextTestRunner as TestRunner
263        test_args = {}
264
265    models = sys.argv[1:]
266    if models and models[0] == '-v':
267        verbosity = 2
268        models = models[1:]
269    else:
270        verbosity = 1
271    if models and models[0] == 'opencl':
272        if not HAVE_OPENCL:
273            print("opencl is not available")
274            return 1
275        loaders = ['opencl']
276        models = models[1:]
277    elif models and models[0] == 'dll':
278        # TODO: test if compiler is available?
279        loaders = ['dll']
280        models = models[1:]
281    elif models and models[0] == 'opencl_and_dll':
282        loaders = ['opencl', 'dll']
283        models = models[1:]
284    else:
285        loaders = ['opencl', 'dll']
286    if not models:
287        print("""\
288usage:
289  python -m sasmodels.model_test [-v] [opencl|dll] model1 model2 ...
290
291If -v is included on the command line, then use verbose output.
292
293If neither opencl nor dll is specified, then models will be tested with
294both OpenCL and dll; the compute target is ignored for pure python models.
295
296If model1 is 'all', then all except the remaining models will be tested.
297
298""")
299
300        return 1
301
302    runner = TestRunner(verbosity=verbosity, **test_args)
303    result = runner.run(make_suite(loaders, models))
304    return 1 if result.failures or result.errors else 0
305
306
307def model_tests():
308    # type: () -> Iterator[Callable[[], None]]
309    """
310    Test runner visible to nosetests.
311
312    Run "nosetests sasmodels" on the command line to invoke it.
313    """
314    tests = make_suite(['opencl', 'dll'], ['all'])
315    for test_i in tests:
316        yield test_i.run_all
317
318
319if __name__ == "__main__":
320    sys.exit(main())
Note: See TracBrowser for help on using the repository browser.