diff options
Diffstat (limited to 'silx/opencl/test')
-rw-r--r-- | silx/opencl/test/__init__.py | 7 | ||||
-rw-r--r-- | silx/opencl/test/test_backprojection.py | 78 | ||||
-rw-r--r-- | silx/opencl/test/test_kahan.py | 269 | ||||
-rw-r--r-- | silx/opencl/test/test_stats.py | 114 |
4 files changed, 451 insertions, 17 deletions
diff --git a/silx/opencl/test/__init__.py b/silx/opencl/test/__init__.py index 2fe88ea..f3121d5 100644 --- a/silx/opencl/test/__init__.py +++ b/silx/opencl/test/__init__.py @@ -24,7 +24,7 @@ __authors__ = ["J. Kieffer"] __license__ = "MIT" -__date__ = "17/10/2017" +__date__ = "11/01/2019" import os import unittest @@ -36,6 +36,9 @@ from . import test_linalg from . import test_array_utils from ..codec import test as test_codec from . import test_image +from . import test_kahan +from . import test_stats + def suite(): test_suite = unittest.TestSuite() @@ -47,6 +50,8 @@ def suite(): test_suite.addTests(test_array_utils.suite()) test_suite.addTests(test_codec.suite()) test_suite.addTests(test_image.suite()) + test_suite.addTests(test_kahan.suite()) + test_suite.addTests(test_stats.suite()) # Allow to remove sift from the project test_base_dir = os.path.dirname(__file__) sift_dir = os.path.join(test_base_dir, "..", "sift") diff --git a/silx/opencl/test/test_backprojection.py b/silx/opencl/test/test_backprojection.py index 70ce2ae..7ab416d 100644 --- a/silx/opencl/test/test_backprojection.py +++ b/silx/opencl/test/test_backprojection.py @@ -35,8 +35,9 @@ __date__ = "19/01/2018" import time import logging -import numpy +import numpy as np import unittest +from math import pi try: import mako except ImportError: @@ -44,6 +45,7 @@ except ImportError: from ..common import ocl if ocl: from .. import backprojection + from ..sinofilter import compute_fourier_filter from silx.test.utils import utilstest logger = logging.getLogger(__name__) @@ -55,7 +57,7 @@ def generate_coords(img_shp, center=None): The zero is at the center of the image. """ l_r, l_c = float(img_shp[0]), float(img_shp[1]) - R, C = numpy.mgrid[:l_r, :l_c] + R, C = np.mgrid[:l_r, :l_c] if center is None: center0, center1 = l_r / 2., l_c / 2. else: @@ -71,7 +73,7 @@ def clip_circle(img, center=None, radius=None): """ R, C = generate_coords(img.shape, center) M = R * R + C * C - res = numpy.zeros_like(img) + res = np.zeros_like(img) if radius is None: radius = img.shape[0] / 2. - 1 mask = M < radius * radius @@ -85,23 +87,29 @@ class TestFBP(unittest.TestCase): def setUp(self): if ocl is None: return - # ~ if sys.platform.startswith('darwin'): - # ~ self.skipTest("Backprojection is not implemented on CPU for OS X yet") self.getfiles() self.fbp = backprojection.Backprojection(self.sino.shape, profile=True) if self.fbp.compiletime_workgroup_size < 16 * 16: - self.skipTest("Current implementation of OpenCL backprojection is not supported on this platform yet") + self.skipTest("Current implementation of OpenCL backprojection is " + "not supported on this platform yet") + # Astra does not use the same backprojector implementation. + # Therefore, we cannot expect results to be the "same" (up to float32 + # numerical error) + self.tol = 5e-2 + if self.fbp.is_cpu: + # Precision is less when using CPU + self.tol *= 2 def tearDown(self): self.sino = None -# self.fbp.log_profile() + # self.fbp.log_profile() self.fbp = None def getfiles(self): # load sinogram of 512x512 MRI phantom - self.sino = numpy.load(utilstest.getfile("sino500.npz"))["data"] + self.sino = np.load(utilstest.getfile("sino500.npz"))["data"] # load reconstruction made with ASTRA FBP (with filter designed in spatial domain) - self.reference_rec = numpy.load(utilstest.getfile("rec_astra_500.npz"))["data"] + self.reference_rec = np.load(utilstest.getfile("rec_astra_500.npz"))["data"] def measure(self): "Common measurement of timings" @@ -124,8 +132,8 @@ class TestFBP(unittest.TestCase): ref_clipped = clip_circle(self.reference_rec) delta = abs(res_clipped - ref_clipped) bad = delta > 1 -# numpy.save("/tmp/bad.npy", bad.astype(int)) - logger.debug("Absolute difference: %s with %s outlier pixels out of %s", delta.max(), bad.sum(), numpy.prod(bad.shape)) + logger.debug("Absolute difference: %s with %s outlier pixels out of %s" + "", delta.max(), bad.sum(), np.prod(bad.shape)) return delta.max() @unittest.skipUnless(ocl and mako, "pyopencl is missing") @@ -143,21 +151,59 @@ class TestFBP(unittest.TestCase): err = self.compare(res) msg = str("Max error = %e" % err) logger.info(msg) - # TODO: cannot do better than 1e0 ? - # The plain backprojection was much better, so it must be an issue in the filtering process - self.assertTrue(err < 1., "Max error is too high") + self.assertTrue(err < self.tol, "Max error is too high") + # Test multiple reconstructions # ----------------------------- - res0 = numpy.copy(res) + res0 = np.copy(res) for i in range(10): res = self.fbp.filtered_backprojection(self.sino) - errmax = numpy.max(numpy.abs(res - res0)) + errmax = np.max(np.abs(res - res0)) self.assertTrue(errmax < 1.e-6, "Max error is too high") + @unittest.skipUnless(ocl and mako, "pyopencl is missing") + def test_fbp_filters(self): + """ + Test the different available filters of silx FBP. + """ + avail_filters = [ + "ramlak", "shepp-logan", "cosine", "hamming", + "hann" + ] + # Create a Dirac delta function at a single angle view. + # As the filters are radially invarant: + # - backprojection yields an image where each line is a Dirac. + # - FBP yields an image where each line is the spatial filter + # One can simply filter "dirac" without backprojecting it, but this + # test will also ensure that backprojection behaves well. + dirac = np.zeros_like(self.sino) + na, dw = dirac.shape + dirac[0, dw//2] = na / pi * 2 + + for filter_name in avail_filters: + B = backprojection.Backprojection(dirac.shape, filter_name=filter_name) + r = B(dirac) + # Check that radial invariance is kept + std0 = np.max(np.abs(np.std(r, axis=0))) + self.assertTrue( + std0 < 5.e-6, + "Something wrong with FBP(filter=%s)" % filter_name + ) + # Check that the filter is retrieved + r_f = np.fft.fft(np.fft.fftshift(r[0])).real / 2. # filter factor + ref_filter_f = compute_fourier_filter(dw, filter_name) + errmax = np.max(np.abs(r_f - ref_filter_f)) + logger.info("FBP filter %s: max error=%e" % (filter_name, errmax)) + self.assertTrue( + errmax < 1.e-3, + "Something wrong with FBP(filter=%s)" % filter_name + ) + def suite(): testSuite = unittest.TestSuite() testSuite.addTest(TestFBP("test_fbp")) + testSuite.addTest(TestFBP("test_fbp_filters")) return testSuite diff --git a/silx/opencl/test/test_kahan.py b/silx/opencl/test/test_kahan.py new file mode 100644 index 0000000..bb9ea3f --- /dev/null +++ b/silx/opencl/test/test_kahan.py @@ -0,0 +1,269 @@ +#!/usr/bin/env python +# coding: utf-8 +# +# Project: Azimuthal integration +# https://github.com/silx-kit/pyFAI +# +# Copyright (C) 2015-2019 European Synchrotron Radiation Facility, Grenoble, France +# +# Principal author: Jérôme Kieffer (Jerome.Kieffer@ESRF.eu) +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +"test suite for OpenCL code" + +from __future__ import absolute_import, division, print_function + +__author__ = "Jérôme Kieffer" +__contact__ = "Jerome.Kieffer@ESRF.eu" +__license__ = "MIT" +__copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" +__date__ = "30/01/2019" + + +import unittest +import numpy +import logging +import platform + +logger = logging.getLogger(__name__) +try: + import pyopencl +except ImportError as error: + logger.warning("OpenCL module (pyopencl) is not present, skip tests. %s.", error) + pyopencl = None + +from .. import ocl +if ocl is not None: + from ..utils import read_cl_file + from .. import pyopencl + import pyopencl.array +from ...test.utils import test_options + + +class TestKahan(unittest.TestCase): + """ + Test the kernels for compensated math in OpenCL + """ + + @classmethod + def setUpClass(cls): + if not test_options.WITH_OPENCL_TEST: + raise unittest.SkipTest("User request to skip OpenCL tests") + if pyopencl is None or ocl is None: + raise unittest.SkipTest("OpenCL module (pyopencl) is not present or no device available") + + cls.ctx = ocl.create_context(devicetype="GPU") + cls.queue = pyopencl.CommandQueue(cls.ctx, properties=pyopencl.command_queue_properties.PROFILING_ENABLE) + + # this is running 32 bits OpenCL woth POCL + if (platform.machine() in ("i386", "i686", "x86_64") and (tuple.__itemsize__ == 4) and + cls.ctx.devices[0].platform.name == 'Portable Computing Language'): + cls.args = "-DX87_VOLATILE=volatile" + else: + cls.args = "" + + @classmethod + def tearDownClass(cls): + cls.queue = None + cls.ctx = None + + @staticmethod + def dummy_sum(ary, dtype=None): + "perform the actual sum in a dummy way " + if dtype is None: + dtype = ary.dtype.type + sum_ = dtype(0) + for i in ary: + sum_ += i + return sum_ + + def test_kahan(self): + # simple test + N = 26 + data = (1 << (N - 1 - numpy.arange(N))).astype(numpy.float32) + + ref64 = numpy.sum(data, dtype=numpy.float64) + ref32 = self.dummy_sum(data) + if (ref64 == ref32): + logger.warning("Kahan: invalid tests as float32 provides the same result as float64") + # Dummy kernel to evaluate + src = """ + kernel void summation(global float* data, + int size, + global float* result) + { + float2 acc = (float2)(0.0f, 0.0f); + for (int i=0; i<size; i++) + { + acc = kahan_sum(acc, data[i]); + } + result[0] = acc.s0; + result[1] = acc.s1; + } + """ + prg = pyopencl.Program(self.ctx, read_cl_file("kahan.cl") + src).build(self.args) + ones_d = pyopencl.array.to_device(self.queue, data) + res_d = pyopencl.array.zeros(self.queue, 2, numpy.float32) + evt = prg.summation(self.queue, (1,), (1,), ones_d.data, numpy.int32(N), res_d.data) + evt.wait() + res = res_d.get().sum(dtype=numpy.float64) + self.assertEqual(ref64, res, "test_kahan") + + def test_dot16(self): + # simple test + N = 16 + data = (1 << (N - 1 - numpy.arange(N))).astype(numpy.float32) + + ref64 = numpy.dot(data.astype(numpy.float64), data.astype(numpy.float64)) + ref32 = numpy.dot(data, data) + if (ref64 == ref32): + logger.warning("dot16: invalid tests as float32 provides the same result as float64") + # Dummy kernel to evaluate + src = """ + kernel void test_dot16(global float* data, + int size, + global float* result) + { + float2 acc = (float2)(0.0f, 0.0f); + float16 data16 = (float16) (data[0],data[1],data[2],data[3],data[4], + data[5],data[6],data[7],data[8],data[9], + data[10],data[11],data[12],data[13],data[14],data[15]); + acc = comp_dot16(data16, data16); + result[0] = acc.s0; + result[1] = acc.s1; + } + + kernel void test_dot8(global float* data, + int size, + global float* result) + { + float2 acc = (float2)(0.0f, 0.0f); + float8 data0 = (float8) (data[0],data[2],data[4],data[6],data[8],data[10],data[12],data[14]); + float8 data1 = (float8) (data[1],data[3],data[5],data[7],data[9],data[11],data[13],data[15]); + acc = comp_dot8(data0, data1); + result[0] = acc.s0; + result[1] = acc.s1; + } + + kernel void test_dot4(global float* data, + int size, + global float* result) + { + float2 acc = (float2)(0.0f, 0.0f); + float4 data0 = (float4) (data[0],data[4],data[8],data[12]); + float4 data1 = (float4) (data[3],data[7],data[11],data[15]); + acc = comp_dot4(data0, data1); + result[0] = acc.s0; + result[1] = acc.s1; + } + + kernel void test_dot3(global float* data, + int size, + global float* result) + { + float2 acc = (float2)(0.0f, 0.0f); + float3 data0 = (float3) (data[0],data[4],data[12]); + float3 data1 = (float3) (data[3],data[11],data[15]); + acc = comp_dot3(data0, data1); + result[0] = acc.s0; + result[1] = acc.s1; + } + + kernel void test_dot2(global float* data, + int size, + global float* result) + { + float2 acc = (float2)(0.0f, 0.0f); + float2 data0 = (float2) (data[0],data[14]); + float2 data1 = (float2) (data[1],data[15]); + acc = comp_dot2(data0, data1); + result[0] = acc.s0; + result[1] = acc.s1; + } + + """ + + prg = pyopencl.Program(self.ctx, read_cl_file("kahan.cl") + src).build(self.args) + ones_d = pyopencl.array.to_device(self.queue, data) + res_d = pyopencl.array.zeros(self.queue, 2, numpy.float32) + evt = prg.test_dot16(self.queue, (1,), (1,), ones_d.data, numpy.int32(N), res_d.data) + evt.wait() + res = res_d.get().sum(dtype="float64") + self.assertEqual(ref64, res, "test_dot16") + + res_d.fill(0) + data0 = data[0::2] + data1 = data[1::2] + ref64 = numpy.dot(data0.astype(numpy.float64), data1.astype(numpy.float64)) + ref32 = numpy.dot(data0, data1) + if (ref64 == ref32): + logger.warning("dot8: invalid tests as float32 provides the same result as float64") + evt = prg.test_dot8(self.queue, (1,), (1,), ones_d.data, numpy.int32(N), res_d.data) + evt.wait() + res = res_d.get().sum(dtype="float64") + self.assertEqual(ref64, res, "test_dot8") + + res_d.fill(0) + data0 = data[0::4] + data1 = data[3::4] + ref64 = numpy.dot(data0.astype(numpy.float64), data1.astype(numpy.float64)) + ref32 = numpy.dot(data0, data1) + if (ref64 == ref32): + logger.warning("dot4: invalid tests as float32 provides the same result as float64") + evt = prg.test_dot4(self.queue, (1,), (1,), ones_d.data, numpy.int32(N), res_d.data) + evt.wait() + res = res_d.get().sum(dtype="float64") + self.assertEqual(ref64, res, "test_dot4") + + res_d.fill(0) + data0 = numpy.array([data[0], data[4], data[12]]) + data1 = numpy.array([data[3], data[11], data[15]]) + ref64 = numpy.dot(data0.astype(numpy.float64), data1.astype(numpy.float64)) + ref32 = numpy.dot(data0, data1) + if (ref64 == ref32): + logger.warning("dot3: invalid tests as float32 provides the same result as float64") + evt = prg.test_dot3(self.queue, (1,), (1,), ones_d.data, numpy.int32(N), res_d.data) + evt.wait() + res = res_d.get().sum(dtype="float64") + self.assertEqual(ref64, res, "test_dot3") + + res_d.fill(0) + data0 = numpy.array([data[0], data[14]]) + data1 = numpy.array([data[1], data[15]]) + ref64 = numpy.dot(data0.astype(numpy.float64), data1.astype(numpy.float64)) + ref32 = numpy.dot(data0, data1) + if (ref64 == ref32): + logger.warning("dot2: invalid tests as float32 provides the same result as float64") + evt = prg.test_dot2(self.queue, (1,), (1,), ones_d.data, numpy.int32(N), res_d.data) + evt.wait() + res = res_d.get().sum(dtype="float64") + self.assertEqual(ref64, res, "test_dot2") + + +def suite(): + testsuite = unittest.TestSuite() + loader = unittest.defaultTestLoader.loadTestsFromTestCase + testsuite.addTest(loader(TestKahan)) + return testsuite + + +if __name__ == '__main__': + runner = unittest.TextTestRunner() + runner.run(suite()) diff --git a/silx/opencl/test/test_stats.py b/silx/opencl/test/test_stats.py new file mode 100644 index 0000000..b5127c8 --- /dev/null +++ b/silx/opencl/test/test_stats.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Project: Sift implementation in Python + OpenCL +# https://github.com/silx-kit/silx +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + +""" +Simple test of an addition +""" + +from __future__ import division, print_function + +__authors__ = ["Henri Payno, Jérôme Kieffer"] +__contact__ = "jerome.kieffer@esrf.eu" +__license__ = "MIT" +__copyright__ = "2013 European Synchrotron Radiation Facility, Grenoble, France" +__date__ = "13/12/2018" + +import logging +import time +import numpy + +import unittest +from ..common import ocl +if ocl: + import pyopencl + import pyopencl.array + from ..statistics import StatResults, Statistics +from ..utils import get_opencl_code +logger = logging.getLogger(__name__) + + +@unittest.skipUnless(ocl, "PyOpenCl is missing") +class TestStatistics(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.size = 1 << 20 # 1 million elements + cls.data = numpy.random.randint(0, 65000, cls.size).astype("uint16") + t0 = time.time() + cls.ref = StatResults(cls.data.min(), cls.data.max(), cls.data.size, + cls.data.sum(), cls.data.mean(), cls.data.std() ** 2, + cls.data.std()) + t1 = time.time() + cls.ref_time = t1 - t0 + + @classmethod + def tearDownClass(cls): + cls.size = cls.ref = cls.data = cls.ref_time = None + + @classmethod + def validate(cls, res): + return ( + (res.min == cls.ref.min) and + (res.max == cls.ref.max) and + (res.cnt == cls.ref.cnt) and + abs(res.mean - cls.ref.mean) < 0.01 and + abs(res.std - cls.ref.std) < 0.1) + + def test_measurement(self): + """ + tests that all devices are working properly ... + """ + logger.info("Reference results: %s", self.ref) + for pid, platform in enumerate(ocl.platforms): + for did, device in enumerate(platform.devices): + try: + s = Statistics(template=self.data, platformid=pid, deviceid=did) + except Exception as err: + failed_init = True + res = StatResults(0,0,0,0,0,0,0) + else: + failed_init = False + t0 = time.time() + res = s(self.data) + t1 = time.time() + logger.warning("failed_init %s", failed_init) + if failed_init or not self.validate(res): + logger.error("Failed on platform %s device %s", platform, device) + logger.error("Reference results: %s", self.ref) + logger.error("Faulty results: %s", res) + self.assertTrue(False, "Stat calculation failed on %s %s" % (platform, device)) + logger.info("Runtime on %s/%s : %.3fms x%.1f", platform, device, 1000 * (t1 - t0), self.ref_time / (t1 - t0)) + + +def suite(): + testSuite = unittest.TestSuite() + testSuite.addTest(TestStatistics("test_measurement")) + return testSuite + + +if __name__ == '__main__': + unittest.main(defaultTest="suite") |