diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 72b927ff70..7f376608e2 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -2,7 +2,7 @@ name: Benchmarks on: push: - branches: [ master ] + branches: [ pyccel:master ] jobs: diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 0a3ec6354d..ca27a35737 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -2,7 +2,7 @@ name: master_tests on: push: - branches: [ master ] + branches: [ pyccel:master ] jobs: diff --git a/performance.md b/performance.md index 3a666bec9c..5b7f7eae87 100644 --- a/performance.md +++ b/performance.md @@ -1,32 +1,32 @@ -# Performance Comparison (as of Tue Jun 14 13:19:45 UTC 2022) +# Performance Comparison (as of Sat Jul 16 08:44:11 UTC 2022) ## Compilation time Algorithm | python | pythran | numba | pyccel | pyccel_c ------------------------- | ------------------------- | ------------------------- | ------------------------- | ------------------------- | ------------------------- -Ackermann | - | 3.70 | 0.48 | 1.53 | 1.37 -Bellman Ford | - | 5.05 | 1.00 | 2.21 | 2.16 -Dijkstra | - | 6.25 | 1.30 | 2.34 | - -Euler | - | 7.64 | 1.52 | 2.29 | 2.27 -Midpoint Explicit | - | 8.69 | 2.21 | 2.79 | 2.69 -Midpoint Fixed | - | 9.61 | 2.65 | 2.77 | 2.70 -RK4 | - | 9.16 | 2.62 | 3.18 | - -FD - L Convection | - | 3.95 | 0.43 | 2.04 | 2.03 -FD - NL Convection | - | 4.15 | 0.44 | 2.11 | 2.34 -FD - Poisson | - | 10.87 | 1.00 | 2.35 | 2.21 -FD - Laplace | - | 18.45 | 2.12 | 2.90 | - -M-D | - | 15.12 | 5.81 | 3.69 | 3.29 +Ackermann | - | 3.32 | 0.39 | 1.39 | 1.29 +Bellman Ford | - | 4.43 | 0.86 | 1.99 | 2.02 +Dijkstra | - | 5.71 | 1.23 | 2.07 | - +Euler | - | 6.60 | 1.36 | 2.00 | 1.98 +Midpoint Explicit | - | 7.59 | 1.94 | 2.39 | 2.44 +Midpoint Fixed | - | 9.13 | 2.28 | 2.55 | 2.46 +RK4 | - | 8.26 | 2.36 | 3.12 | - +FD - L Convection | - | 3.54 | 0.39 | 1.96 | 1.86 +FD - NL Convection | - | 3.50 | 0.39 | 1.88 | 1.86 +FD - Poisson | - | 9.20 | 0.86 | 2.14 | 1.98 +FD - Laplace | - | 16.55 | 1.89 | 2.59 | - +M-D | - | 11.96 | 5.13 | 2.88 | 2.58 ## Execution time Algorithm | python | pythran | numba | pyccel | pyccel_c ------------------------- | ------------------------- | ------------------------- | ------------------------- | ------------------------- | ------------------------- -Ackermann (ms) | 528.00 $\pm$ 16.00 | 18.10 $\pm$ 1.20 | 34.60 $\pm$ 1.70 | 3.65 $\pm$ 0.12 | 4.15 $\pm$ 0.14 -Bellman Ford (ns) | 77200.00 $\pm$ 3900.00 | 470.00 $\pm$ 18.00 | 639.00 $\pm$ 22.00 | 282.00 $\pm$ 11.00 | 603.00 $\pm$ 53.00 -Dijkstra (ns) | 40600.00 $\pm$ 1200.00 | 418.00 $\pm$ 21.00 | 418.00 $\pm$ 27.00 | 350.00 $\pm$ 12.00 | - -Euler (ms) | 72.20 $\pm$ 5.10 | 0.75 $\pm$ 0.03 | 1.02 $\pm$ 0.04 | 0.24 $\pm$ 0.02 | 3.62 $\pm$ 0.13 -Midpoint Explicit (ms) | 147.00 $\pm$ 9.00 | 1.66 $\pm$ 0.05 | 2.69 $\pm$ 0.08 | 0.31 $\pm$ 0.01 | 6.54 $\pm$ 0.58 -Midpoint Fixed (ms) | 721.00 $\pm$ 45.00 | 9.49 $\pm$ 0.32 | 14.90 $\pm$ 0.60 | 1.09 $\pm$ 0.03 | 26.50 $\pm$ 0.90 -RK4 (ms) | 324.00 $\pm$ 17.00 | 2.48 $\pm$ 0.08 | 5.21 $\pm$ 0.21 | 0.94 $\pm$ 0.03 | - -FD - L Convection (ms) | 2560.00 $\pm$ 50.00 | 2.09 $\pm$ 0.04 | 11.60 $\pm$ 0.60 | 2.35 $\pm$ 0.09 | 2.25 $\pm$ 0.06 -FD - NL Convection (ms) | 3760.00 $\pm$ 130.00 | 2.05 $\pm$ 0.06 | 11.70 $\pm$ 0.60 | 2.05 $\pm$ 0.13 | 2.33 $\pm$ 0.07 -FD - Poisson (ms) | 5670.00 $\pm$ 180.00 | 3.78 $\pm$ 0.11 | 14.40 $\pm$ 0.80 | 5.06 $\pm$ 0.12 | 3.61 $\pm$ 0.12 -FD - Laplace (\textmu s) | 112.00 $\pm$ 8.00 | 3.15 $\pm$ 0.14 | 12.20 $\pm$ 0.30 | 2.79 $\pm$ 0.14 | - -M-D (ms) | 66800.00 $\pm$ 1700.00 | 77.80 $\pm$ 2.60 | 280.00 $\pm$ 16.00 | 69.50 $\pm$ 1.80 | 82.40 $\pm$ 4.50 +Ackermann (ms) | 478.00 $\pm$ 15.00 | 18.70 $\pm$ 0.10 | 31.20 $\pm$ 0.40 | 10.60 $\pm$ 0.00 | 10.20 $\pm$ 0.10 +Bellman Ford (ns) | 66400.00 $\pm$ 3100.00 | 379.00 $\pm$ 1.00 | 563.00 $\pm$ 14.00 | 239.00 $\pm$ 7.00 | 487.00 $\pm$ 6.00 +Dijkstra (ns) | 34100.00 $\pm$ 500.00 | 360.00 $\pm$ 6.00 | 344.00 $\pm$ 3.00 | 274.00 $\pm$ 0.00 | - +Euler (ms) | 57.60 $\pm$ 0.80 | 0.59 $\pm$ 0.01 | 1.11 $\pm$ 0.02 | 0.14 $\pm$ 0.00 | 3.12 $\pm$ 0.07 +Midpoint Explicit (ms) | 118.00 $\pm$ 2.00 | 1.36 $\pm$ 0.02 | 2.85 $\pm$ 0.07 | 0.17 $\pm$ 0.00 | 5.60 $\pm$ 0.17 +Midpoint Fixed (ms) | 588.00 $\pm$ 18.00 | 8.72 $\pm$ 0.06 | 15.90 $\pm$ 0.30 | 0.68 $\pm$ 0.01 | 25.10 $\pm$ 0.80 +RK4 (ms) | 275.00 $\pm$ 22.00 | 1.87 $\pm$ 0.00 | 5.76 $\pm$ 0.17 | 0.79 $\pm$ 0.04 | - +FD - L Convection (ms) | 2190.00 $\pm$ 60.00 | 1.48 $\pm$ 0.03 | 9.39 $\pm$ 0.06 | 1.65 $\pm$ 0.02 | 1.57 $\pm$ 0.02 +FD - NL Convection (ms) | 3150.00 $\pm$ 90.00 | 1.96 $\pm$ 0.03 | 9.25 $\pm$ 0.06 | 1.72 $\pm$ 0.01 | 1.84 $\pm$ 0.02 +FD - Poisson (ms) | 4570.00 $\pm$ 170.00 | 2.06 $\pm$ 0.03 | 10.50 $\pm$ 0.10 | 3.97 $\pm$ 0.11 | 1.78 $\pm$ 0.01 +FD - Laplace (\textmu s) | 60.90 $\pm$ 2.90 | 2.36 $\pm$ 0.03 | 8.72 $\pm$ 0.13 | 2.22 $\pm$ 0.03 | - +M-D (ms) | 52500.00 $\pm$ 600.00 | 55.20 $\pm$ 0.50 | 219.00 $\pm$ 1.00 | 48.20 $\pm$ 0.20 | 61.60 $\pm$ 0.60 diff --git a/pyccel/ast/builtins.py b/pyccel/ast/builtins.py index 09d9f60f34..90a1205bd4 100644 --- a/pyccel/ast/builtins.py +++ b/pyccel/ast/builtins.py @@ -22,8 +22,8 @@ from .literals import LiteralInteger, LiteralFloat, LiteralComplex, Nil from .literals import Literal, LiteralImaginaryUnit, get_default_literal_value from .literals import LiteralString -from .operators import PyccelAdd, PyccelAnd, PyccelMul, PyccelIsNot -from .operators import PyccelMinus, PyccelUnarySub, PyccelNot +from .operators import PyccelAdd, PyccelAnd, PyccelMul, PyccelIsNot, PyccelDiv +from .operators import PyccelMinus, PyccelUnarySub, PyccelNot, PyccelPow from .variable import IndexedElement pyccel_stage = PyccelStage() @@ -51,6 +51,7 @@ 'PythonZip', 'PythonMax', 'PythonMin', + 'PythonRound', 'python_builtin_datatype' ) @@ -360,6 +361,47 @@ def arg(self): def __str__(self): return 'float({0})'.format(str(self.arg)) +# =========================================================================== +class PythonRound(PyccelAstNode): + """ Represents a call to Python's native round() function. + """ + __slots__ = ('_arg', '_ndigits', '_dtype', '_precision') + name = 'round' + _rank = 0 + _shape = () + _order = None + _attribute_nodes = ('_arg','_ndigits') + + def __init__(self, number, ndigits = None): + self._arg = number + if ndigits is None: + self._dtype = NativeInteger() + else: + self._dtype = number.dtype + self._ndigits = ndigits + self._precision = -1 + super().__init__() + + @property + def arg(self): + """ Number to be rounded + """ + return self._arg + + @property + def ndigits(self): + """ Number of digits to which the argument is rounded + """ + return self._ndigits + + def get_round_with_0_digits(self): + """ Get expression returning the same value but containing + a call to PyccelRound with ndigits=None + """ + assert self.ndigits is not None + factor = PyccelPow(LiteralFloat(10), self.ndigits) + return PyccelDiv(PythonRound(PyccelMul(self.arg, factor)), factor) + #============================================================================== class PythonInt(PyccelAstNode): """ Represents a call to Python's native int() function. @@ -948,4 +990,5 @@ def python_builtin_datatype(name): 'not' : PyccelNot, 'map' : PythonMap, 'type' : PythonType, + 'round' : PythonRound, } diff --git a/pyccel/codegen/printing/ccode.py b/pyccel/codegen/printing/ccode.py index 7b0b5c64d2..c819be8e6e 100644 --- a/pyccel/codegen/printing/ccode.py +++ b/pyccel/codegen/printing/ccode.py @@ -549,6 +549,14 @@ def _print_PythonAbs(self, expr): func = "labs" return "{}({})".format(func, self._print(expr.arg)) + def _print_PythonRound(self, expr): + if expr.ndigits is None: + self.add_import(c_imports['math']) + arg = self._print(expr.arg) + return f"lrint({arg})" + else: + return self._print(expr.get_round_with_0_digits()) + def _print_PythonMin(self, expr): arg = expr.args[0] if arg.dtype is NativeFloat() and len(arg) == 2: diff --git a/pyccel/codegen/printing/fcode.py b/pyccel/codegen/printing/fcode.py index 687eafcce5..97a033b718 100644 --- a/pyccel/codegen/printing/fcode.py +++ b/pyccel/codegen/printing/fcode.py @@ -797,6 +797,28 @@ def _print_PythonAbs(self, expr): """ return "abs({})".format(self._print(expr.arg)) + def _print_PythonRound(self, expr): + """ print the python builtin function round + args : variable + """ + arg = expr.arg + ndigits = expr.ndigits + self._additional_imports.add(Import('pyc_math_f90', Module('pyc_math_f90',(),()))) + if arg.precision != -1: + arg = DtypePrecisionToCastFunction[arg.dtype.name][arg.precision] + + arg_code = self._print(arg) + if ndigits: + if ndigits.precision != -1: + ndigits = DtypePrecisionToCastFunction[ndigits.dtype.name][ndigits.precision] + + ndigits_code = self._print(ndigits) + return f"pyc_bankers_round({arg_code}, {ndigits_code})" + else: + prec = self.print_kind(expr) + zero = self._print(LiteralInteger(0)) + return f"Int(pyc_bankers_round({arg_code}, {zero}), kind={prec})" + def _print_PythonTuple(self, expr): shape = tuple(reversed(expr.shape)) if len(shape)>1: diff --git a/pyccel/codegen/printing/pycode.py b/pyccel/codegen/printing/pycode.py index 149b97360f..503ee23b67 100644 --- a/pyccel/codegen/printing/pycode.py +++ b/pyccel/codegen/printing/pycode.py @@ -407,6 +407,14 @@ def _print_PythonConjugate(self, expr): def _print_PythonPrint(self, expr): return 'print({})\n'.format(', '.join(self._print(a) for a in expr.expr)) + def _print_PythonRound(self, expr): + arg = self._print(expr.arg) + if expr.ndigits: + ndigits = self._print(expr.ndigits) + return f'round({arg}, {ndigits})' + else: + return f'round({arg})' + def _print_PyccelArraySize(self, expr): arg = self._print(expr.arg) index = self._print(expr.index) diff --git a/pyccel/commands/console.py b/pyccel/commands/console.py index 22e9f8afd9..23c7499802 100644 --- a/pyccel/commands/console.py +++ b/pyccel/commands/console.py @@ -221,11 +221,13 @@ def pyccel(files=None, mpi=None, openmp=None, openacc=None, output_dir=None, com # ... # ... + # this will initialize the singelton ErrorsMode + # making this settings available everywhere + err_mode = ErrorsMode() if args.developer_mode: - # this will initialize the singelton ErrorsMode - # making this settings available everywhere - err_mode = ErrorsMode() err_mode.set_mode('developer') + else: + err_mode.set_mode('user') # ... base_dirpath = os.getcwd() diff --git a/pyccel/epyccel.py b/pyccel/epyccel.py index fecc7cca52..1548198bf4 100644 --- a/pyccel/epyccel.py +++ b/pyccel/epyccel.py @@ -264,11 +264,13 @@ def epyccel( python_function_or_module, **kwargs ): comm = kwargs.pop('comm', None) root = kwargs.pop('root', 0) bcast = kwargs.pop('bcast', True) + # This will initialize the singleton ErrorsMode + # making this setting available everywhere + err_mode = ErrorsMode() if kwargs.pop('developer_mode', None): - # This will initialize the singleton ErrorsMode - # making this setting available everywhere - err_mode = ErrorsMode() err_mode.set_mode('developer') + else: + err_mode.set_mode('user') # Parallel version if comm is not None: diff --git a/pyccel/stdlib/math/pyc_math_f90.f90 b/pyccel/stdlib/math/pyc_math_f90.f90 index d413df6083..13fdfa8983 100644 --- a/pyccel/stdlib/math/pyc_math_f90.f90 +++ b/pyccel/stdlib/math/pyc_math_f90.f90 @@ -5,10 +5,12 @@ module pyc_math_f90 -use ISO_C_BINDING +use, intrinsic :: ISO_C_BINDING implicit none +private + real(C_DOUBLE), parameter, private :: pi = 4.0_C_DOUBLE * DATAN(1.0_C_DOUBLE) interface pyc_gcd @@ -26,6 +28,20 @@ module pyc_math_f90 module procedure pyc_lcm_8 end interface pyc_lcm +interface pyc_bankers_round + module procedure pyc_bankers_round_4 + module procedure pyc_bankers_round_8 + module procedure pyc_bankers_round_int_4 + module procedure pyc_bankers_round_int_8 +end interface pyc_bankers_round + +public :: pyc_gcd, & + pyc_factorial, & + pyc_lcm, & + pyc_radians, & + pyc_degrees, & + pyc_bankers_round + contains ! Implementation of math factorial function @@ -156,4 +172,88 @@ pure function pyc_degrees(rad) result(deg) end function pyc_degrees +pure function pyc_bankers_round_4(arg, ndigits) result(rnd) + + implicit none + + real(C_DOUBLE), value :: arg + integer(C_INT32_T), value :: ndigits + real(C_DOUBLE) :: rnd + + real(C_DOUBLE) :: diff + + arg = arg * 10._C_DOUBLE**ndigits + + rnd = nint(arg, kind=C_INT64_T) + + diff = arg - rnd + + if (ndigits <= 0 .and. (diff == 0.5_C_DOUBLE .or. diff == -0.5_C_DOUBLE)) then + rnd = nint(arg*0.5_C_DOUBLE, kind=C_INT64_T)*2_C_INT64_T + end if + + rnd = rnd * 10._C_DOUBLE**(-ndigits) + +end function pyc_bankers_round_4 + +pure function pyc_bankers_round_8(arg, ndigits) result(rnd) + + implicit none + + real(C_DOUBLE), value :: arg + integer(C_INT64_T), value :: ndigits + real(C_DOUBLE) :: rnd + + real(C_DOUBLE) :: diff + + arg = arg * 10._C_DOUBLE**ndigits + + rnd = nint(arg, kind=C_INT64_T) + + diff = arg - rnd + + if (ndigits <= 0 .and. (diff == 0.5_C_DOUBLE .or. diff == -0.5_C_DOUBLE)) then + rnd = nint(arg*0.5_C_DOUBLE, kind=C_INT64_T)*2_C_INT64_T + end if + + rnd = rnd * 10._C_DOUBLE**(-ndigits) + +end function pyc_bankers_round_8 + +pure function pyc_bankers_round_int_4(arg, ndigits) result(rnd) + + implicit none + + integer(C_INT32_T), value :: arg + integer(C_INT32_T), value :: ndigits + integer(C_INT32_T) :: rnd + + real(C_DOUBLE) :: val + + val = arg * 10._C_DOUBLE**ndigits + + rnd = nint(val, kind=C_INT64_T) + + rnd = rnd * 10._C_DOUBLE**(-ndigits) + +end function pyc_bankers_round_int_4 + +pure function pyc_bankers_round_int_8(arg, ndigits) result(rnd) + + implicit none + + integer(C_INT64_T), value :: arg + integer(C_INT64_T), value :: ndigits + integer(C_INT64_T) :: rnd + + real(C_DOUBLE) :: val + + val = arg * 10._C_DOUBLE**ndigits + + rnd = nint(val, kind=C_INT64_T) + + rnd = rnd * 10._C_DOUBLE**(-ndigits) + +end function pyc_bankers_round_int_8 + end module pyc_math_f90 diff --git a/tests/epyccel/test_epyccel_round.py b/tests/epyccel/test_epyccel_round.py new file mode 100644 index 0000000000..8dcae13317 --- /dev/null +++ b/tests/epyccel/test_epyccel_round.py @@ -0,0 +1,191 @@ +# pylint: disable=missing-function-docstring, missing-module-docstring/ +import pytest +import numpy as np +from numpy.random import randint + +from pyccel.decorators import types +from pyccel.epyccel import epyccel + +def test_round_int(language): + @types('float') + def round_int(x): + return round(x) + + f = epyccel(round_int, language=language) + x = randint(100) / 10 + + f_output = f(x) + round_int_output = round_int(x) + assert round_int_output == f_output + assert isinstance(f_output, type(round_int_output)) + + # Round down + x = 3.345 + + f_output = f(x) + round_int_output = round_int(x) + assert round_int_output == f_output + assert isinstance(f_output, type(round_int_output)) + + # Round up + x = 3.845 + + f_output = f(x) + round_int_output = round_int(x) + assert round_int_output == f_output + assert isinstance(f_output, type(round_int_output)) + + # Round half + x = 6.5 + + f_output = f(x) + round_int_output = round_int(x) + assert round_int_output == f_output + assert isinstance(f_output, type(round_int_output)) + +def test_negative_round_int(language): + @types('float') + def round_int(x): + return round(x) + + f = epyccel(round_int, language=language) + x = -randint(100) / 10 + + f_output = f(x) + round_int_output = round_int(x) + assert round_int_output == f_output + assert isinstance(f_output, type(round_int_output)) + + # Round up + x = -3.345 + + f_output = f(x) + round_int_output = round_int(x) + assert round_int_output == f_output + assert isinstance(f_output, type(round_int_output)) + + # Round down + x = -3.845 + + f_output = f(x) + round_int_output = round_int(x) + assert round_int_output == f_output + assert isinstance(f_output, type(round_int_output)) + + # Round half + x = -6.5 + + f_output = f(x) + round_int_output = round_int(x) + assert round_int_output == f_output + assert isinstance(f_output, type(round_int_output)) + +def test_round_ndigits(language): + @types('float','int') + def round_ndigits(x, i): + return round(x,i) + + f = epyccel(round_ndigits, language=language) + x = randint(100) / 10 + + f_output = f(x, 1) + round_ndigits_output = round_ndigits(x, 1) + assert np.isclose(round_ndigits_output, f_output) + assert isinstance(f_output, type(round_ndigits_output)) + + x = 3.343 + + f_output = f(x,2) + round_ndigits_output = round_ndigits(x,2) + assert np.isclose(round_ndigits_output, f_output) + assert isinstance(f_output, type(round_ndigits_output)) + + x = 3323.0 + + f_output = f(x,-2) + round_ndigits_output = round_ndigits(x, -2) + assert np.isclose(round_ndigits_output, f_output) + assert isinstance(f_output, type(round_ndigits_output)) + + x = -3390.0 + + f_output = f(x,-2) + round_ndigits_output = round_ndigits(x, -2) + assert np.isclose(round_ndigits_output, f_output) + assert isinstance(f_output, type(round_ndigits_output)) + +@pytest.mark.parametrize( 'language', ( + pytest.param('fortran', marks = pytest.mark.fortran), + pytest.param('c', marks = [ + pytest.mark.xfail(reason="Python implements bankers' round. But only for ndigits=0"), + pytest.mark.c]), + pytest.param('python', marks = pytest.mark.python), + ) +) +def test_round_ndigits_half(language): + @types('float','int') + def round_ndigits(x, i): + return round(x,i) + + f = epyccel(round_ndigits, language=language) + x = randint(100) / 10 + + f_output = f(x, 1) + round_ndigits_output = round_ndigits(x, 1) + assert np.isclose(round_ndigits_output, f_output) + assert isinstance(f_output, type(round_ndigits_output)) + + x = 3.345 + + f_output = f(x,2) + round_ndigits_output = round_ndigits(x,2) + assert np.isclose(round_ndigits_output, f_output) + assert isinstance(f_output, type(round_ndigits_output)) + + x = -3350.0 + + f_output = f(x,-2) + round_ndigits_output = round_ndigits(x, -2) + assert np.isclose(round_ndigits_output, f_output) + assert isinstance(f_output, type(round_ndigits_output)) + + x = 45.0 + + f_output = f(x,-1) + round_ndigits_output = round_ndigits(x,-1) + assert np.isclose(round_ndigits_output, f_output) + assert isinstance(f_output, type(round_ndigits_output)) + +def test_round_ndigits_int(language): + @types('int','int') + def round_ndigits(x, i): + return round(x,i) + + f = epyccel(round_ndigits, language=language) + x = randint(100) // 10 + + f_output = f(x, 1) + round_ndigits_output = round_ndigits(x, 1) + assert np.isclose(round_ndigits_output, f_output) + assert isinstance(f_output, type(round_ndigits_output)) + + x = 3 + + f_output = f(x,2) + round_ndigits_output = round_ndigits(x,2) + assert np.isclose(round_ndigits_output, f_output) + assert isinstance(f_output, type(round_ndigits_output)) + + x = 3323 + + f_output = f(x,-2) + round_ndigits_output = round_ndigits(x, -2) + assert np.isclose(round_ndigits_output, f_output) + assert isinstance(f_output, type(round_ndigits_output)) + + x = -3390 + + f_output = f(x,-2) + round_ndigits_output = round_ndigits(x, -2) + assert np.isclose(round_ndigits_output, f_output) + assert isinstance(f_output, type(round_ndigits_output))