From 587ce851c4a6d5b9e5e0e4a85c33b6b29a846171 Mon Sep 17 00:00:00 2001 From: "A. R. Shajii" Date: Mon, 4 Oct 2021 11:08:28 -0400 Subject: [PATCH] Add more cmath tests --- stdlib/cmath.codon | 54 ++-- stdlib/internal/types/float.codon | 8 +- stdlib/internal/types/int.codon | 8 +- stdlib/math.codon | 7 +- test/stdlib/cmath_test.codon | 496 ++++++++++++++++++++++++++++-- 5 files changed, 520 insertions(+), 53 deletions(-) diff --git a/stdlib/cmath.codon b/stdlib/cmath.codon index f0c3086f..90348775 100644 --- a/stdlib/cmath.codon +++ b/stdlib/cmath.codon @@ -245,15 +245,15 @@ def _rect_special(): C(INF,-0.), C(INF,0.), C(U,U), C(INF,N), C(INF,N), C(N,N), C(N,N), C(N,0.), C(N,0.), C(N,N), C(N,N), C(N,N)) return v -def _is_special(z: complex): +def _is_special(z): return (not math.isfinite(z.real)) or (not math.isfinite(z.imag)) -def _special_get(z: complex, table): +def _special_get(z, table): t1 = _special_type(z.real) t2 = _special_type(z.imag) return table[7*t1 + t2] -def _sqrt_impl(z: complex): +def _sqrt_impl(z): if _is_special(z): return _special_get(z, _sqrt_special()) @@ -285,7 +285,7 @@ def _sqrt_impl(z: complex): # errno = 0 return complex(r_real, r_imag) -def _acos_impl(z: complex): +def _acos_impl(z): if _is_special(z): return _special_get(z, _acos_special()) @@ -307,7 +307,7 @@ def _acos_impl(z: complex): r_imag = math.asinh(s2.real*s1.imag - s2.imag*s1.real) return complex(r_real, r_imag) -def _acosh_impl(z: complex): +def _acosh_impl(z): if _is_special(z): return _special_get(z, _acosh_special()) @@ -324,7 +324,7 @@ def _acosh_impl(z: complex): r_imag = 2.*math.atan2(s1.imag, s2.real) return complex(r_real, r_imag) -def _asinh_impl(z: complex): +def _asinh_impl(z): if _is_special(z): return _special_get(z, _asinh_special()) @@ -343,13 +343,13 @@ def _asinh_impl(z: complex): r_imag = math.atan2(z.imag, s1.real*s2.real - s1.imag*s2.imag) return complex(r_real, r_imag) -def _asin_impl(z: complex): +def _asin_impl(z): s = _asinh_impl(complex(-z.imag, z.real)) r_real = s.imag r_imag = -s.real return complex(r_real, r_imag) -def _atanh_impl(z: complex): +def _atanh_impl(z): if _is_special(z): return _special_get(z, _atanh_special()) @@ -388,13 +388,13 @@ def _atanh_impl(z: complex): # errno = 0 return complex(r_real, r_imag) -def _atan_impl(z: complex): +def _atan_impl(z): s = _atanh_impl(complex(-z.imag, z.real)) r_real = s.imag r_imag = -s.real return complex(r_real, r_imag) -def _cosh_impl(z: complex): +def _cosh_impl(z): r_real = 0. r_imag = 0. # special treatment for cosh(+/-inf + iy) if y is not a NaN @@ -438,11 +438,11 @@ def _cosh_impl(z: complex): ''' return complex(r_real, r_imag) -def _cos_impl(z: complex): +def _cos_impl(z): r = _cosh_impl(complex(-z.imag, z.real)) return r -def _exp_impl(z: complex): +def _exp_impl(z): r_real = 0. r_imag = 0. if (not math.isfinite(z.real)) or (not math.isfinite(z.imag)): @@ -486,7 +486,7 @@ def _exp_impl(z: complex): ''' return complex(r_real, r_imag) -def _c_log(z: complex): +def _c_log(z): if _is_special(z): return _special_get(z, _log_special()) @@ -519,11 +519,11 @@ def _c_log(z: complex): # errno = 0 return complex(r_real, r_imag) -def _log10_impl(z: complex): +def _log10_impl(z): s = _c_log(z) return complex(s.real / _M_LN10, s.imag / _M_LN10) -def _sinh_impl(z: complex): +def _sinh_impl(z): r_real = 0. r_imag = 0. if (not math.isfinite(z.real)) or (not math.isfinite(z.imag)): @@ -564,12 +564,12 @@ def _sinh_impl(z: complex): ''' return complex(r_real, r_imag) -def _sin_impl(z: complex): +def _sin_impl(z): s = _sinh_impl(complex(-z.imag, z.real)) r = complex(s.imag, -s.real) return r -def _tanh_impl(z: complex): +def _tanh_impl(z): r_real = 0. r_imag = 0. # special treatment for tanh(+/-inf + iy) if y is finite and @@ -611,17 +611,11 @@ def _tanh_impl(z: complex): # errno = 0 return complex(r_real, r_imag) -def _tan_impl(z: complex): +def _tan_impl(z): s = _tanh_impl(complex(-z.imag, z.real)) r = complex(s.imag, -s.real) return r -def _log_impl(x: complex, y: complex): - x = _c_log(x) - y = _c_log(y) - x /= y - return x - def phase(x): z = complex(x) return z._phase() @@ -672,10 +666,14 @@ def exp(x): z = complex(x) return _exp_impl(z) -def log(x): - # TODO: base +def log(x, base = e): z = complex(x) - return _c_log(z) + y = complex(base) + r = _c_log(z) + if y == complex(e, 0.0): + return r + else: + return r/_c_log(y) def log10(x): z = complex(x) @@ -683,7 +681,7 @@ def log10(x): def sqrt(x): z = complex(x) - return _sqrt_impl(x) + return _sqrt_impl(z) def asin(x): z = complex(x) diff --git a/stdlib/internal/types/float.codon b/stdlib/internal/types/float.codon index 4e479741..f9d16240 100644 --- a/stdlib/internal/types/float.codon +++ b/stdlib/internal/types/float.codon @@ -331,5 +331,9 @@ class float: return result def __match__(self, i: float): return self == i - def __suffix_j__(s: str): - return complex(0.0, float(s)) + @property + def real(self): + return self + @property + def imag(self): + return 0.0 diff --git a/stdlib/internal/types/int.codon b/stdlib/internal/types/int.codon index b9d34a92..9d1793a3 100644 --- a/stdlib/internal/types/int.codon +++ b/stdlib/internal/types/int.codon @@ -329,5 +329,9 @@ class int: ret i64 %tmp def __match__(self, i: int): return self == i - def __suffix_j__(s: str): - return complex(0, int(s)) + @property + def real(self): + return self + @property + def imag(self): + return 0 diff --git a/stdlib/math.codon b/stdlib/math.codon index f4af9f3e..6737d04b 100644 --- a/stdlib/math.codon +++ b/stdlib/math.codon @@ -135,7 +135,7 @@ def ldexp(x: float, i: int) -> float: """ return _C.ldexp(x, i) -def log(x: float) -> float: +def log(x: float, base: float = e) -> float: """ log(float) -> float @@ -148,7 +148,10 @@ def log(x: float) -> float: %y = call double @llvm.log.f64(double %x) ret double %y - return f(x) + if base == e: + return f(x) + else: + return f(x)/f(base) def log2(x: float) -> float: """ diff --git a/test/stdlib/cmath_test.codon b/test/stdlib/cmath_test.codon index 59aed4ca..e36824f7 100644 --- a/test/stdlib/cmath_test.codon +++ b/test/stdlib/cmath_test.codon @@ -1,32 +1,491 @@ import math import cmath -def check(exp, got, flags): - def close(a, b): - if math.isnan(a): - return math.isnan(b) - elif math.isnan(b): - return math.isnan(a) - return math.isclose(a, b, rel_tol = 1e-10, abs_tol=1e-15) +INF = float('inf') +NAN = float('nan') +j = complex(0, 1) - x1 = exp.real - y1 = exp.imag +complex_zeros = [complex(x, y) for x in [0.0, -0.0] for y in [0.0, -0.0]] +complex_infinities = [complex(x, y) for x, y in [ + (INF, 0.0), # 1st quadrant + (INF, 2.3), + (INF, INF), + (2.3, INF), + (0.0, INF), + (-0.0, INF), # 2nd quadrant + (-2.3, INF), + (-INF, INF), + (-INF, 2.3), + (-INF, 0.0), + (-INF, -0.0), # 3rd quadrant + (-INF, -2.3), + (-INF, -INF), + (-2.3, -INF), + (-0.0, -INF), + (0.0, -INF), # 4th quadrant + (2.3, -INF), + (INF, -INF), + (INF, -2.3), + (INF, -0.0) + ]] +complex_nans = [complex(x, y) for x, y in [ + (NAN, -INF), + (NAN, -2.3), + (NAN, -0.0), + (NAN, 0.0), + (NAN, 2.3), + (NAN, INF), + (-INF, NAN), + (-2.3, NAN), + (-0.0, NAN), + (0.0, NAN), + (2.3, NAN), + (INF, NAN) + ]] - x2 = got.real - y2 = got.imag +def float_identical(x, y): + if math.isnan(x) or math.isnan(y): + if math.isnan(x) and math.isnan(y): + return True + elif x == y: + if x != 0.0: + return True + # both zero; check that signs match + elif math.copysign(1.0, x) == math.copysign(1.0, y): + return True + else: + return False + return False - if 'ignore-real-sign' in flags: - x1 = math.fabs(x1) - x2 = math.fabs(x2) +def complex_identical(x, y): + return float_identical(x.real, y.real) and float_identical(x.imag, y.imag) - if 'ignore-imag-sign' in flags: - y1 = math.fabs(y1) - y2 = math.fabs(y2) +@llvm +@pure +def small() -> float: + ret double 4.940660e-323 - return close(x1, x2) and close(y1, y2) +def almost_equal(a, b, rel_err = 2e-15, abs_err = small()): + if math.isnan(a): + if math.isnan(b): + return True + return False + + if math.isinf(a): + if a == b: + return True + return False + + if not a and not b: + if math.copysign(1., a) != math.copysign(1., b): + return False + + absolute_error = abs(b-a) + if absolute_error <= max(abs_err, rel_err * abs(a)): + return True + return False + +@test +def test_constants(): + e_expected = 2.71828182845904523536 + pi_expected = 3.14159265358979323846 + assert math.isclose(cmath.pi, pi_expected) + assert math.isclose(cmath.e, e_expected) +test_constants() + +@test +def test_infinity_and_nan_constants(): + assert cmath.inf.real == math.inf + assert cmath.inf.imag == 0.0 + assert cmath.infj.real == 0.0 + assert cmath.infj.imag == math.inf + + assert math.isnan(cmath.nan.real) + assert cmath.nan.imag == 0.0 + assert cmath.nanj.real == 0.0 + assert math.isnan(cmath.nanj.imag) + + assert str(cmath.inf) == "inf" + assert str(cmath.infj) == "infj" + assert str(cmath.nan) == "nan" + assert str(cmath.nanj) == "nanj" +test_infinity_and_nan_constants() + +@test +def test_user_object(): + class MyComplexOS[T]: + value: T + def __init__(self, value: T): + self.value = value + def __complex__(self): + return self.value + + x = MyComplexOS(4.2) + assert cmath.acos(x) == cmath.acos(x.value) + assert cmath.acosh(x) == cmath.acosh(x.value) + assert cmath.asin(x) == cmath.asin(x.value) + assert cmath.asinh(x) == cmath.asinh(x.value) + assert cmath.atan(x) == cmath.atan(x.value) + assert cmath.atanh(x) == cmath.atanh(x.value) + assert cmath.cos(x) == cmath.cos(x.value) + assert cmath.cosh(x) == cmath.cosh(x.value) + assert cmath.exp(x) == cmath.exp(x.value) + assert cmath.log(x) == cmath.log(x.value) + assert cmath.log10(x) == cmath.log10(x.value) + assert cmath.sin(x) == cmath.sin(x.value) + assert cmath.sinh(x) == cmath.sinh(x.value) + assert cmath.sqrt(x) == cmath.sqrt(x.value) + assert cmath.tan(x) == cmath.tan(x.value) + assert cmath.tanh(x) == cmath.tanh(x.value) +test_user_object() + +@test +def test_input_type(): + x = 42 + y = float(x) + assert cmath.acos(x) == cmath.acos(y) + assert cmath.acosh(x) == cmath.acosh(y) + assert cmath.asin(x) == cmath.asin(y) + assert cmath.asinh(x) == cmath.asinh(y) + assert cmath.atan(x) == cmath.atan(y) + assert cmath.atanh(x) == cmath.atanh(y) + assert cmath.cos(x) == cmath.cos(y) + assert cmath.cosh(x) == cmath.cosh(y) + assert cmath.exp(x) == cmath.exp(y) + assert cmath.log(x) == cmath.log(y) + assert cmath.log10(x) == cmath.log10(y) + assert cmath.sin(x) == cmath.sin(y) + assert cmath.sinh(x) == cmath.sinh(y) + assert cmath.sqrt(x) == cmath.sqrt(y) + assert cmath.tan(x) == cmath.tan(y) + assert cmath.tanh(x) == cmath.tanh(y) +test_input_type() + + +@test +def test_cmath_matches_math(): + test_values = [0.01, 0.1, 0.2, 0.5, 0.9, 0.99] + unit_interval = test_values + [-x for x in test_values] + \ + [0., 1., -1.] + positive = test_values + [1.] + [1./x for x in test_values] + nonnegative = [0.] + positive + real_line = [0.] + positive + [-x for x in positive] + + test_functions = { + 'acos' : unit_interval, + 'asin' : unit_interval, + 'atan' : real_line, + 'cos' : real_line, + 'cosh' : real_line, + 'exp' : real_line, + 'log' : positive, + 'log10' : positive, + 'sin' : real_line, + 'sinh' : real_line, + 'sqrt' : nonnegative, + 'tan' : real_line, + 'tanh' : real_line} + + for v in test_functions['acos']: + z = cmath.acos(v) + assert almost_equal(z.real, math.acos(v)) + assert z.imag == 0. + + for v in test_functions['asin']: + z = cmath.asin(v) + assert almost_equal(z.real, math.asin(v)) + assert z.imag == 0. + + for v in test_functions['atan']: + z = cmath.atan(v) + assert almost_equal(z.real, math.atan(v)) + assert z.imag == 0. + + for v in test_functions['cos']: + z = cmath.cos(v) + assert almost_equal(z.real, math.cos(v)) + assert z.imag == 0. + + for v in test_functions['cosh']: + z = cmath.cosh(v) + assert almost_equal(z.real, math.cosh(v)) + assert z.imag == 0. + + for v in test_functions['exp']: + z = cmath.exp(v) + assert almost_equal(z.real, math.exp(v)) + assert z.imag == 0. + + for v in test_functions['log']: + z = cmath.log(v) + assert almost_equal(z.real, math.log(v)) + assert z.imag == 0. + + for v in test_functions['log10']: + z = cmath.log10(v) + assert almost_equal(z.real, math.log10(v)) + assert z.imag == 0. + + for v in test_functions['sin']: + z = cmath.sin(v) + assert almost_equal(z.real, math.sin(v)) + assert z.imag == 0. + + for v in test_functions['sinh']: + z = cmath.sinh(v) + assert almost_equal(z.real, math.sinh(v)) + assert z.imag == 0. + + for v in test_functions['sqrt']: + z = cmath.sqrt(v) + assert almost_equal(z.real, math.sqrt(v)) + assert z.imag == 0. + + for v in test_functions['tan']: + z = cmath.tan(v) + assert almost_equal(z.real, math.tan(v)) + assert z.imag == 0. + + for v in test_functions['tanh']: + z = cmath.tanh(v) + assert almost_equal(z.real, math.tanh(v)) + assert z.imag == 0. + + for base in [0.5, 2., 10.]: + for v in positive: + z = cmath.log(v, base) + s = math.log(v, base) + # added 'or z.real == s' since Codon version gives -0 vs. +0 in one test + assert almost_equal(z.real, math.log(v, base)) or z.real == s + assert z.imag == 0. +test_cmath_matches_math() + +@test +def test_polar(): + def check(arg, expected): + got = cmath.polar(arg) + return all(almost_equal(e, g) for e,g in zip(expected, got)) + pi = cmath.pi + assert check(0, (0., 0.)) + assert check(1, (1., 0.)) + assert check(-1, (1., pi)) + assert check(1*j, (1., pi / 2)) + assert check(-3*j, (3., -pi / 2)) + inf = float('inf') + assert check(complex(inf, 0), (inf, 0.)) + assert check(complex(-inf, 0), (inf, pi)) + assert check(complex(3, inf), (inf, pi / 2)) + assert check(complex(5, -inf), (inf, -pi / 2)) + assert check(complex(inf, inf), (inf, pi / 4)) + assert check(complex(inf, -inf), (inf, -pi / 4)) + assert check(complex(-inf, inf), (inf, 3 * pi / 4)) + assert check(complex(-inf, -inf), (inf, -3 * pi / 4)) + nan = float('nan') + assert check(complex(nan, 0), (nan, nan)) + assert check(complex(0, nan), (nan, nan)) + assert check(complex(nan, nan), (nan, nan)) + assert check(complex(inf, nan), (inf, nan)) + assert check(complex(-inf, nan), (inf, nan)) + assert check(complex(nan, inf), (inf, nan)) + assert check(complex(nan, -inf), (inf, nan)) +test_polar() + +@test +def test_phase(): + from cmath import phase, pi + assert almost_equal(phase(0), 0.) + assert almost_equal(phase(1.), 0.) + assert almost_equal(phase(-1.), pi) + assert almost_equal(phase(-1.+1E-300*j), pi) + assert almost_equal(phase(-1.-1E-300*j), -pi) + assert almost_equal(phase(1*j), pi/2) + assert almost_equal(phase(-1*j), -pi/2) + + # zeros + assert phase(complex(0.0, 0.0)) == 0.0 + assert phase(complex(0.0, -0.0)) == -0.0 + assert phase(complex(-0.0, 0.0)) == pi + assert phase(complex(-0.0, -0.0)) == -pi + + # infinities + assert almost_equal(phase(complex(-INF, -0.0)), -pi) + assert almost_equal(phase(complex(-INF, -2.3)), -pi) + assert almost_equal(phase(complex(-INF, -INF)), -0.75*pi) + assert almost_equal(phase(complex(-2.3, -INF)), -pi/2) + assert almost_equal(phase(complex(-0.0, -INF)), -pi/2) + assert almost_equal(phase(complex(0.0, -INF)), -pi/2) + assert almost_equal(phase(complex(2.3, -INF)), -pi/2) + assert almost_equal(phase(complex(INF, -INF)), -pi/4) + assert phase(complex(INF, -2.3)) == -0.0 + assert phase(complex(INF, -0.0)) == -0.0 + assert phase(complex(INF, 0.0)) == 0.0 + assert phase(complex(INF, 2.3)) == 0.0 + assert almost_equal(phase(complex(INF, INF)), pi/4) + assert almost_equal(phase(complex(2.3, INF)), pi/2) + assert almost_equal(phase(complex(0.0, INF)), pi/2) + assert almost_equal(phase(complex(-0.0, INF)), pi/2) + assert almost_equal(phase(complex(-2.3, INF)), pi/2) + assert almost_equal(phase(complex(-INF, INF)), 0.75*pi) + assert almost_equal(phase(complex(-INF, 2.3)), pi) + assert almost_equal(phase(complex(-INF, 0.0)), pi) + + # real or imaginary part NaN + for z in complex_nans: + assert math.isnan(phase(z)) +test_phase() + +@test +def test_abs(): + # zeros + for z in complex_zeros: + assert abs(z) == 0.0 + + # infinities + for z in complex_infinities: + assert abs(z) == INF + + # real or imaginary part NaN + assert abs(complex(NAN, -INF)) == INF + assert math.isnan(abs(complex(NAN, -2.3))) + assert math.isnan(abs(complex(NAN, -0.0))) + assert math.isnan(abs(complex(NAN, 0.0))) + assert math.isnan(abs(complex(NAN, 2.3))) + assert abs(complex(NAN, INF)) == INF + assert abs(complex(-INF, NAN)) == INF + assert math.isnan(abs(complex(-2.3, NAN))) + assert math.isnan(abs(complex(-0.0, NAN))) + assert math.isnan(abs(complex(0.0, NAN))) + assert math.isnan(abs(complex(2.3, NAN))) + assert abs(complex(INF, NAN)) == INF + assert math.isnan(abs(complex(NAN, NAN))) +test_abs() + +def c_equal(a, b): + eps = 1E-7 + if abs(a.real - b[0]) > eps or abs(a.imag - b[1]) > eps: + return False + return True + +@test +def test_rect(): + from cmath import rect, pi + assert c_equal(rect(0, 0), (0, 0)) + assert c_equal(rect(1, 0), (1., 0)) + assert c_equal(rect(1, -pi), (-1., 0)) + assert c_equal(rect(1, pi/2), (0, 1.)) + assert c_equal(rect(1, -pi/2), (0, -1.)) +test_rect() + +@test +def test_isfinite(): + real_vals = [float('-inf'), -2.3, -0.0, + 0.0, 2.3, float('inf'), float('nan')] + for x in real_vals: + for y in real_vals: + z = complex(x, y) + assert cmath.isfinite(z) == (math.isfinite(x) and math.isfinite(y)) +test_isfinite() + +@test +def test_isnan(): + assert not cmath.isnan(1) + assert not cmath.isnan(1*j) + assert not cmath.isnan(INF) + assert cmath.isnan(NAN) + assert cmath.isnan(complex(NAN, 0)) + assert cmath.isnan(complex(0, NAN)) + assert cmath.isnan(complex(NAN, NAN)) + assert cmath.isnan(complex(NAN, INF)) + assert cmath.isnan(complex(INF, NAN)) +test_isnan() + +@test +def test_isinf(): + assert not cmath.isinf(1) + assert not cmath.isinf(1*j) + assert not cmath.isinf(NAN) + assert cmath.isinf(INF) + assert cmath.isinf(complex(INF, 0)) + assert cmath.isinf(complex(0, INF)) + assert cmath.isinf(complex(INF, INF)) + assert cmath.isinf(complex(NAN, INF)) + assert cmath.isinf(complex(INF, NAN)) +test_isinf() + +@test +def test_tanh_sign(): + for z in complex_zeros: + assert complex_identical(cmath.tanh(z), z) +test_tanh_sign() + +@test +def test_atan_sign(): + for z in complex_zeros: + assert complex_identical(cmath.atan(z), z) +test_atan_sign() + +@test +def test_atanh_sign(): + for z in complex_zeros: + assert complex_identical(cmath.atanh(z), z) +test_atanh_sign() + +@test +def test_is_close(): + # test complex values that are close to within 12 decimal places + complex_examples = [(1.0+1.0*j, 1.000000000001+1.0*j), + (1.0+1.0*j, 1.0+1.000000000001*j), + (-1.0+1.0*j, -1.000000000001+1.0*j), + (1.0-1.0*j, 1.0-0.999999999999*j), + ] + + for a,b in complex_examples: + assert cmath.isclose(a, b, rel_tol=1e-12) + assert not cmath.isclose(a, b, rel_tol=1e-13) + + # test values near zero that are near to within three decimal places + near_zero_examples = [(0.001*j, 0), + (0.001 + 0*j, 0), + (0.001+0.001*j, 0), + (-0.001+0.001*j, 0), + (0.001-0.001*j, 0), + (-0.001-0.001*j, 0), + ] + + for a,b in near_zero_examples: + assert cmath.isclose(a, b, abs_tol=1.5e-03) + assert not cmath.isclose(a, b, abs_tol=0.5e-03) + + assert cmath.isclose(0.001-0.001*j, 0.001+0.001*j, abs_tol=2e-03) + assert not cmath.isclose(0.001-0.001*j, 0.001+0.001*j, abs_tol=1e-03) +test_is_close() @test def test_cmath_testcases(): + def check(exp, got, flags): + def close(a, b): + if math.isnan(a): + return math.isnan(b) + elif math.isnan(b): + return math.isnan(a) + return math.isclose(a, b, rel_tol = 1e-10, abs_tol=1e-15) + + x1 = exp.real + y1 = exp.imag + + x2 = got.real + y2 = got.imag + + if 'ignore-real-sign' in flags: + x1 = math.fabs(x1) + x2 = math.fabs(x2) + + if 'ignore-imag-sign' in flags: + y1 = math.fabs(y1) + y2 = math.fabs(y2) + + return close(x1, x2) and close(y1, y2) + def run_test(test): v = test.split() if not v: @@ -91,5 +550,4 @@ def test_cmath_testcases(): for test in tests: assert run_test(test) - test_cmath_testcases()