Skip to content

Commit e963fcb

Browse files
nandiniraja348Nandini RajaEmilyBourne
authored
Issue pyccel#983 | Add Round function and implement the conversion to C, Fortran, and Python (pyccel#996)
Add support for Python's `round` function which is implemented with a banker's round. An integer and a floating point implementation are both required to avoid floating point errors when handling integers. Fixes pyccel#983. A minor bug fix is also included to ensure that the error mode is set back to 'user' when nothing is specified if the developer mode was used previously. --------- Co-authored-by: Nandini Raja <[email protected]> Co-authored-by: Emily.Bourne <[email protected]>
1 parent 054f8db commit e963fcb

File tree

13 files changed

+395
-17
lines changed

13 files changed

+395
-17
lines changed

AUTHORS

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,5 @@ Contributors
3838
* Mohammed Ayaz Shaikh
3939
* Amir Movassaghi
4040
* Leonardo Merlin Paloschi
41-
* Joao Antonio Gomes
41+
* Joao Antonio Gomes
42+
* Nandini Raja

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ All notable changes to this project will be documented in this file.
6161
- Add support for inhomogeneous tuple annotations.
6262
- #1834 : Add support for `@property` decorator.
6363
- #2099 : Fix translation of modules containing `__all__`.
64+
- #983 : Add support for built-in function `round`.
6465
- \[INTERNALS\] Add abstract class `SetMethod` to handle calls to various set methods.
6566
- \[INTERNALS\] Added `container_rank` property to `ast.datatypes.PyccelType` objects.
6667
- \[INTERNALS\] Add a `__call__` method to `FunctionDef` to create `FunctionCall` instances.

docs/builtin-functions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ Python contains a limited number of builtin functions defined [here](https://doc
5959
| **`range`** | **Yes** |
6060
| `repr` | No |
6161
| `reversed` | No |
62-
| `round` | No |
62+
| **`round`** | **Yes** |
6363
| *`set`* | Python-only |
6464
| `setattr` | No |
6565
| `slice` | No |

pyccel/ast/builtins.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616

1717
from .basic import PyccelAstNode, TypedAstNode
1818
from .datatypes import PythonNativeInt, PythonNativeBool, PythonNativeFloat
19-
from .datatypes import GenericType, PythonNativeComplex, PrimitiveComplexType
19+
from .datatypes import GenericType, PythonNativeComplex
20+
from .datatypes import PrimitiveBooleanType, PrimitiveComplexType
2021
from .datatypes import HomogeneousTupleType, InhomogeneousTupleType
2122
from .datatypes import HomogeneousListType, HomogeneousContainerType
2223
from .datatypes import FixedSizeNumericType, HomogeneousSetType, SymbolicType
@@ -53,6 +54,7 @@
5354
'PythonPrint',
5455
'PythonRange',
5556
'PythonReal',
57+
'PythonRound',
5658
'PythonSet',
5759
'PythonSetFunction',
5860
'PythonSum',
@@ -480,6 +482,52 @@ def arg(self):
480482
def __str__(self):
481483
return f'float({self.arg})'
482484

485+
# ===========================================================================
486+
class PythonRound(PyccelFunction):
487+
"""
488+
Class representing a call to Python's native round() function.
489+
490+
Class representing a call to Python's native round() function
491+
which rounds a float or integer to a given number of decimals.
492+
493+
Parameters
494+
----------
495+
number : TypedAstNode
496+
The number to be rounded.
497+
ndigits : TypedAstNode, optional
498+
The number of digits to round to.
499+
"""
500+
__slots__ = ('_class_type',)
501+
name = 'round'
502+
_rank = 0
503+
_shape = None
504+
_order = None
505+
506+
def __init__(self, number, ndigits = None):
507+
if ndigits is None or number.class_type.primitive_type is PrimitiveBooleanType():
508+
self._class_type = PythonNativeInt()
509+
else:
510+
self._class_type = number.class_type
511+
super().__init__(number, ndigits)
512+
513+
@property
514+
def arg(self):
515+
"""
516+
The number to be rounded.
517+
518+
The number to be rounded.
519+
"""
520+
return self._args[0]
521+
522+
@property
523+
def ndigits(self):
524+
"""
525+
The number of digits to which the argument is rounded.
526+
527+
The number of digits to which the argument is rounded.
528+
"""
529+
return self._args[1]
530+
483531
#==============================================================================
484532
class PythonInt(PyccelFunction):
485533
"""
@@ -1664,6 +1712,7 @@ def get_assign_targets(self):
16641712
'min' : PythonMin,
16651713
'not' : PyccelNot,
16661714
'range' : PythonRange,
1715+
'round' : PythonRound,
16671716
'set' : PythonSetFunction,
16681717
'str' : LiteralString,
16691718
'sum' : PythonSum,

pyccel/codegen/printing/ccode.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,15 @@ def _print_PythonAbs(self, expr):
749749
func = "labs"
750750
return "{}({})".format(func, self._print(expr.arg))
751751

752+
def _print_PythonRound(self, expr):
753+
self.add_import(c_imports['pyc_math_c'])
754+
arg = self._print(expr.arg)
755+
ndigits = self._print(expr.ndigits or LiteralInteger(0))
756+
if isinstance(expr.arg.class_type.primitive_type, (PrimitiveBooleanType, PrimitiveIntegerType)):
757+
return f'ipyc_bankers_round({arg}, {ndigits})'
758+
else:
759+
return f'fpyc_bankers_round({arg}, {ndigits})'
760+
752761
def _print_PythonMinMax(self, expr):
753762
arg = expr.args[0]
754763
if arg.dtype.primitive_type is PrimitiveFloatingPointType() and len(arg) == 2:

pyccel/codegen/printing/fcode.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@
7171
from pyccel.ast.numpyext import NumpySign
7272
from pyccel.ast.numpyext import NumpyIsFinite, NumpyIsNan
7373

74-
from pyccel.ast.numpytypes import NumpyNDArrayType
74+
from pyccel.ast.numpytypes import NumpyNDArrayType, NumpyInt64Type
7575

7676
from pyccel.ast.operators import PyccelAdd, PyccelMul, PyccelMinus, PyccelAnd, PyccelEq
7777
from pyccel.ast.operators import PyccelMod, PyccelNot, PyccelAssociativeParenthesis
@@ -1198,6 +1198,25 @@ def _print_PythonAbs(self, expr):
11981198
arg_code = self._get_node_without_gFTL(expr.arg)
11991199
return f"abs({arg_code})"
12001200

1201+
def _print_PythonRound(self, expr):
1202+
arg = expr.arg
1203+
if not isinstance(arg.dtype.primitive_type, PrimitiveFloatingPointType):
1204+
arg = self._apply_cast(NumpyInt64Type(), arg)
1205+
self.add_import(Import('pyc_math_f90', Module('pyc_math_f90',(),())))
1206+
1207+
arg_code = self._print(arg)
1208+
ndigits = self._apply_cast(NumpyInt64Type(), expr.ndigits) if expr.ndigits \
1209+
else LiteralInteger(0, NumpyInt64Type())
1210+
ndigits_code = self._print(ndigits)
1211+
1212+
code = f'pyc_bankers_round({arg_code}, {ndigits_code})'
1213+
1214+
if not isinstance(expr.dtype.primitive_type, PrimitiveFloatingPointType):
1215+
prec = self.print_kind(expr)
1216+
return f"Int({code}, kind={prec})"
1217+
else:
1218+
return code
1219+
12011220
def _print_PythonTuple(self, expr):
12021221
shape = tuple(reversed(expr.shape))
12031222
if len(shape)>1:

pyccel/codegen/printing/pycode.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,14 @@ def _print_PyccelArrayShapeElement(self, expr):
507507
else:
508508
raise NotImplementedError("The shape access function seems to be poorly defined.")
509509

510+
def _print_PythonRound(self, expr):
511+
arg = self._print(expr.arg)
512+
if expr.ndigits:
513+
ndigits = self._print(expr.ndigits)
514+
return f'round({arg}, {ndigits})'
515+
else:
516+
return f'round({arg})'
517+
510518
def _print_PyccelArraySize(self, expr):
511519
arg = self._print(expr.arg)
512520
name = self._get_numpy_name(expr)

pyccel/commands/console.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -248,11 +248,13 @@ def pyccel(files=None, mpi=None, openmp=None, openacc=None, output_dir=None, com
248248
# ...
249249

250250
# ...
251+
# this will initialize the singelton ErrorsMode
252+
# making this settings available everywhere
253+
err_mode = ErrorsMode()
251254
if args.developer_mode:
252-
# this will initialize the singelton ErrorsMode
253-
# making this settings available everywhere
254-
err_mode = ErrorsMode()
255255
err_mode.set_mode('developer')
256+
else:
257+
err_mode.set_mode('user')
256258
# ...
257259

258260
base_dirpath = os.getcwd()

pyccel/commands/epyccel.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -330,11 +330,13 @@ def epyccel( python_function_or_module, **kwargs ):
330330
comm = kwargs.pop('comm', None)
331331
root = kwargs.pop('root', 0)
332332
bcast = kwargs.pop('bcast', True)
333+
# This will initialize the singleton ErrorsMode
334+
# making this setting available everywhere
335+
err_mode = ErrorsMode()
333336
if kwargs.pop('developer_mode', None):
334-
# This will initialize the singleton ErrorsMode
335-
# making this setting available everywhere
336-
err_mode = ErrorsMode()
337337
err_mode.set_mode('developer')
338+
else:
339+
err_mode.set_mode('user')
338340

339341
# Parallel version
340342
if comm is not None:

pyccel/stdlib/math/pyc_math_c.c

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,6 @@ int64_t pyc_lcm (int64_t a, int64_t b)
3939
return a / pyc_gcd(a, b) * b;
4040
}
4141
/*---------------------------------------------------------------------------*/
42-
extern inline double pyc_radians(double degrees);
43-
/*---------------------------------------------------------------------------*/
44-
extern inline double pyc_degrees(double radians);
45-
/*---------------------------------------------------------------------------*/
46-
extern inline int64_t pyc_modulo(int64_t a, int64_t b);
47-
/*---------------------------------------------------------------------------*/
48-
extern inline double pyc_fmodulo(double a, double b);
4942

5043
/* numpy.sign for float, double and integers */
5144
long long int isign(long long int x)
@@ -70,3 +63,42 @@ double complex csign(double complex x)
7063
double absolute = cabs(x);
7164
return ((absolute == 0) ? 0.0 : (x / absolute));
7265
}
66+
67+
/*---------------------------------------------------------------------------*/
68+
69+
double fpyc_bankers_round(double arg, int64_t ndigits)
70+
{
71+
double factor = pow(10.0, ndigits);
72+
arg *= factor;
73+
74+
double nearest_int_fix = copysign(0.5, arg);
75+
76+
double rnd = (int64_t)(arg + nearest_int_fix);
77+
78+
double diff = arg - rnd;
79+
80+
if (ndigits <= 0 && (diff == 0.5 || diff == -0.5)) {
81+
rnd = ((int64_t)(arg*0.5 + nearest_int_fix))*2.0;
82+
}
83+
84+
return rnd / factor;
85+
}
86+
87+
int64_t ipyc_bankers_round(int64_t arg, int64_t ndigits)
88+
{
89+
if (ndigits >= 0) {
90+
return arg;
91+
} else {
92+
int64_t mul_fact = 1;
93+
for (int i = 0; i< -ndigits; ++i) mul_fact *= 10;
94+
95+
int64_t pivot_point = copysign(5*mul_fact/10, arg);
96+
int64_t remainder = arg % mul_fact;
97+
if ( remainder == pivot_point ) {
98+
int64_t val = (mul_fact - remainder) / mul_fact;
99+
return (val + (val & 1)) * mul_fact;
100+
} else {
101+
return ((arg + pivot_point) / mul_fact) * mul_fact;
102+
}
103+
}
104+
}

0 commit comments

Comments
 (0)