From 7b8018287c2ab5e8ec52dbf3e96976331430ab74 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sun, 26 Oct 2025 16:00:06 +0000 Subject: [PATCH 1/5] trignometric tests --- quaddtype/pyproject.toml | 1 + quaddtype/release_tracker.md | 14 +- quaddtype/tests/test_quaddtype.py | 219 ++++++++++++++++++++++++++++++ 3 files changed, 227 insertions(+), 7 deletions(-) diff --git a/quaddtype/pyproject.toml b/quaddtype/pyproject.toml index ff1e1e44..e532bf54 100644 --- a/quaddtype/pyproject.toml +++ b/quaddtype/pyproject.toml @@ -33,6 +33,7 @@ dependencies = [ [project.optional-dependencies] test = [ "pytest", + "mpmath", "pytest-run-parallel" ] diff --git a/quaddtype/release_tracker.md b/quaddtype/release_tracker.md index 441f370c..158336b6 100644 --- a/quaddtype/release_tracker.md +++ b/quaddtype/release_tracker.md @@ -40,13 +40,13 @@ | square | ✅ | ✅ | | cbrt | ✅ | ✅ | | reciprocal | ✅ | ✅ | -| sin | ✅ | ❌ _Need: basic tests + edge cases (NaN/inf/0/π multiples/2π range)_ | -| cos | ✅ | ❌ _Need: basic tests + edge cases (NaN/inf/0/π multiples/2π range)_ | -| tan | ✅ | ❌ _Need: basic tests + edge cases (NaN/inf/0/π/2 asymptotes)_ | -| arcsin | ✅ | ❌ _Need: basic tests + edge cases (NaN/inf/±1/out-of-domain)_ | -| arccos | ✅ | ❌ _Need: basic tests + edge cases (NaN/inf/±1/out-of-domain)_ | -| arctan | ✅ | ❌ _Need: basic tests + edge cases (NaN/inf/0/asymptotes)_ | -| arctan2 | ✅ | ❌ _Need: basic tests + edge cases (NaN/inf/0/quadrant coverage)_ | +| sin | ✅ | ✅ | +| cos | ✅ | ✅ | +| tan | ✅ | ✅ | +| arcsin | ✅ | ✅ | +| arccos | ✅ | ✅ | +| arctan | ✅ | ✅ | +| arctan2 | ✅ | ✅ | | hypot | ✅ | ✅ | | sinh | ✅ | ✅ | | cosh | ✅ | ✅ | diff --git a/quaddtype/tests/test_quaddtype.py b/quaddtype/tests/test_quaddtype.py index 375c0c92..6bc0c50d 100644 --- a/quaddtype/tests/test_quaddtype.py +++ b/quaddtype/tests/test_quaddtype.py @@ -3,8 +3,11 @@ import numpy as np import operator +from mpmath import mp + import numpy_quaddtype from numpy_quaddtype import QuadPrecDType, QuadPrecision +from numpy_quaddtype import pi as quad_pi def test_create_scalar_simple(): @@ -1735,6 +1738,222 @@ def test_divmod_broadcasting(): np.testing.assert_allclose(float(quotients[i]), expected_quotients[i], rtol=1e-14) np.testing.assert_allclose(float(remainders[i]), expected_remainders[i], rtol=1e-14) +class TestTrignometricFunctions: + @pytest.mark.parametrize("op", ["sin", "cos", "tan", "atan"]) + @pytest.mark.parametrize("val", [ + # Basic cases + "0.0", "-0.0", "1.0", "-1.0", "2.0", "-2.0", + # pi multiples + str(quad_pi), str(-quad_pi), str(2*quad_pi), str(-2*quad_pi), str(quad_pi/2), str(-quad_pi/2), str(3*quad_pi/2), str(-3*quad_pi/2), + # Small values + "1e-10", "-1e-10", "1e-15", "-1e-15", + # Values near one + "0.9", "-0.9", "0.9999", "-0.9999", + "1.1", "-1.1", "1.0001", "-1.0001", + # Medium values + "10.0", "-10.0", "20.0", "-20.0", + # Large values + "100.0", "200.0", "700.0", "1000.0", "1e100", "1e308", + "-100.0", "-200.0", "-700.0", "-1000.0", "-1e100", "-1e308", + # Fractional values + "0.5", "-0.5", "1.5", "-1.5", "2.5", "-2.5", + # Special values + "inf", "-inf", "nan", + ]) + def test_sin_cos_tan(self, op, val): + mp.prec = 113 # Set precision to 113 bits (~34 decimal digits) + numpy_op = getattr(np, op) + mpmath_op = getattr(mp, op) + + quad_val = QuadPrecision(val) + mpf_val = mp.mpf(val) + + quad_result = numpy_op(quad_val) + mpmath_result = mpmath_op(mpf_val) + # convert mpmath result to quad for comparison + mpmath_result = QuadPrecision(str(mpmath_result)) + + # Handle NaN cases + if np.isnan(mpmath_result): + assert np.isnan(quad_result), f"Expected NaN for {op}({val}), got {quad_result}" + return + + # Handle infinity cases + if np.isinf(mpmath_result): + assert np.isinf(quad_result), f"Expected inf for {op}({val}), got {quad_result}" + assert np.sign(mpmath_result) == np.sign(quad_result), f"Infinity sign mismatch for {op}({val})" + return + + # For finite non-zero results + np.testing.assert_allclose(quad_result, mpmath_result, rtol=1e-32, atol=1e-34, + err_msg=f"Value mismatch for {op}({val}), expected {mpmath_result}, got {quad_result}") + + # their domain is [-1 , 1] + @pytest.mark.parametrize("op", ["asin", "acos"]) + @pytest.mark.parametrize("val", [ + # Basic cases (valid domain) + "0.0", "-0.0", "1.0", "-1.0", + # Small values + "1e-10", "-1e-10", "1e-15", "-1e-15", + # Values near domain boundaries + "0.9", "-0.9", "0.9999", "-0.9999", + "0.99999999", "-0.99999999", + "0.999999999999", "-0.999999999999", + # Fractional values (within domain) + "0.5", "-0.5", + # Special values + "nan" + ]) + def test_inverse_sin_cos(self, op, val): + mp.prec = 113 # Set precision to 113 bits (~34 decimal digits) + numpy_op = getattr(np, op) + mpmath_op = getattr(mp, op) + + quad_val = QuadPrecision(val) + mpf_val = mp.mpf(val) + + quad_result = numpy_op(quad_val) + mpmath_result = mpmath_op(mpf_val) + # convert mpmath result to quad for comparison + mpmath_result = QuadPrecision(str(mpmath_result)) + + # Handle NaN cases + if np.isnan(mpmath_result): + assert np.isnan(quad_result), f"Expected NaN for {op}({val}), got {quad_result}" + return + + # For finite non-zero results + np.testing.assert_allclose(quad_result, mpmath_result, rtol=1e-32, atol=1e-34, + err_msg=f"Value mismatch for {op}({val}), expected {mpmath_result}, got {quad_result}") + + # mpmath's atan2 does not follow IEEE standards so hardcoding the edge cases + # for special edge cases check reference here: https://en.cppreference.com/w/cpp/numeric/math/atan2.html + # atan2: [Real x Real] -> [-pi , pi] + @pytest.mark.parametrize("y", [ + # Basic cases + "0.0", "-0.0", "1.0", "-1.0", + # Small values + "1e-10", "-1e-10", "1e-15", "-1e-15", + # Medium/Large values + "10.0", "-10.0", "100.0", "-100.0", "1000.0", "-1000.0", + # Fractional + "0.5", "-0.5", "2.5", "-2.5", + # Special + "inf", "-inf", "nan", + ]) + @pytest.mark.parametrize("x", [ + "0.0", "-0.0", "1.0", "-1.0", + "1e-10", "-1e-10", + "10.0", "-10.0", "100.0", "-100.0", + "0.5", "-0.5", + "inf", "-inf", "nan", + ]) + def test_atan2(self, y, x): + mp.prec = 113 + + quad_y = QuadPrecision(y) + quad_x = QuadPrecision(x) + mpf_y = mp.mpf(y) + mpf_x = mp.mpf(x) + + quad_result = np.arctan2(quad_y, quad_x) + + # IEEE 754 special cases - hardcoded expectations + y_val = float(y) + x_val = float(x) + + # If either x is NaN or y is NaN, NaN is returned + if np.isnan(y_val) or np.isnan(x_val): + assert np.isnan(quad_result), f"Expected NaN for atan2({y}, {x}), got {quad_result}" + return + + # If y is ±0 and x is negative or -0, ±π is returned + if y_val == 0.0 and (x_val < 0.0 or (x_val == 0.0 and np.signbit(x_val))): + expected = quad_pi if not np.signbit(y_val) else -quad_pi + np.testing.assert_allclose(quad_result, expected, rtol=1e-32, atol=1e-34, + err_msg=f"Value mismatch for atan2({y}, {x}), expected {expected}, got {quad_result}") + return + + # If y is ±0 and x is positive or +0, ±0 is returned + if y_val == 0.0 and (x_val > 0.0 or (x_val == 0.0 and not np.signbit(x_val))): + assert quad_result == 0.0, f"Expected ±0 for atan2({y}, {x}), got {quad_result}" + assert np.signbit(quad_result) == np.signbit(y_val), f"Sign mismatch for atan2({y}, {x})" + return + + # If y is ±∞ and x is finite, ±π/2 is returned + if np.isinf(y_val) and np.isfinite(x_val): + expected = quad_pi / 2 if y_val > 0 else -quad_pi / 2 + np.testing.assert_allclose(quad_result, expected, rtol=1e-32, atol=1e-34, + err_msg=f"Value mismatch for atan2({y}, {x}), expected {expected}, got {quad_result}") + return + + # If y is ±∞ and x is -∞, ±3π/4 is returned + if np.isinf(y_val) and np.isinf(x_val) and x_val < 0: + expected = 3 * quad_pi / 4 if y_val > 0 else -3 * quad_pi / 4 + np.testing.assert_allclose(quad_result, expected, rtol=1e-32, atol=1e-34, + err_msg=f"Value mismatch for atan2({y}, {x}), expected {expected}, got {quad_result}") + return + + # If y is ±∞ and x is +∞, ±π/4 is returned + if np.isinf(y_val) and np.isinf(x_val) and x_val > 0: + expected = quad_pi / 4 if y_val > 0 else -quad_pi / 4 + np.testing.assert_allclose(quad_result, expected, rtol=1e-32, atol=1e-34, + err_msg=f"Value mismatch for atan2({y}, {x}), expected {expected}, got {quad_result}") + return + + # If x is ±0 and y is negative, -π/2 is returned + if x_val == 0.0 and y_val < 0.0: + expected = -quad_pi / 2 + np.testing.assert_allclose(quad_result, expected, rtol=1e-32, atol=1e-34, + err_msg=f"Value mismatch for atan2({y}, {x}), expected {expected}, got {quad_result}") + return + + # If x is ±0 and y is positive, +π/2 is returned + if x_val == 0.0 and y_val > 0.0: + expected = quad_pi / 2 + np.testing.assert_allclose(quad_result, expected, rtol=1e-32, atol=1e-34, + err_msg=f"Value mismatch for atan2({y}, {x}), expected {expected}, got {quad_result}") + return + + # If x is -∞ and y is finite and positive, +π is returned + if np.isinf(x_val) and x_val < 0 and np.isfinite(y_val) and y_val > 0.0: + expected = quad_pi + np.testing.assert_allclose(quad_result, expected, rtol=1e-32, atol=1e-34, + err_msg=f"Value mismatch for atan2({y}, {x}), expected {expected}, got {quad_result}") + return + + # If x is -∞ and y is finite and negative, -π is returned + if np.isinf(x_val) and x_val < 0 and np.isfinite(y_val) and y_val < 0.0: + expected = -quad_pi + np.testing.assert_allclose(quad_result, expected, rtol=1e-32, atol=1e-34, + err_msg=f"Value mismatch for atan2({y}, {x}), expected {expected}, got {quad_result}") + return + + # If x is +∞ and y is finite and positive, +0 is returned + if np.isinf(x_val) and x_val > 0 and np.isfinite(y_val) and y_val > 0.0: + assert quad_result == 0.0 and not np.signbit(quad_result), f"Expected +0 for atan2({y}, {x}), got {quad_result}" + return + + # If x is +∞ and y is finite and negative, -0 is returned + if np.isinf(x_val) and x_val > 0 and np.isfinite(y_val) and y_val < 0.0: + assert quad_result == 0.0 and np.signbit(quad_result), f"Expected -0 for atan2({y}, {x}), got {quad_result}" + return + + # For all other cases, compare with mpmath + mpmath_result = mp.atan2(mpf_y, mpf_x) + mpmath_result = QuadPrecision(str(mpmath_result)) + + if np.isnan(mpmath_result): + assert np.isnan(quad_result), f"Expected NaN for atan2({y}, {x}), got {quad_result}" + return + + if np.isinf(mpmath_result): + assert np.isinf(quad_result), f"Expected inf for atan2({y}, {x}), got {quad_result}" + assert np.sign(mpmath_result) == np.sign(quad_result), f"Infinity sign mismatch for atan2({y}, {x})" + return + + np.testing.assert_allclose(quad_result, mpmath_result, rtol=1e-32, atol=1e-34, + err_msg=f"Value mismatch for atan2({y}, {x}), expected {mpmath_result}, got {quad_result}") @pytest.mark.parametrize("op", ["sinh", "cosh", "tanh", "arcsinh", "arccosh", "arctanh"]) @pytest.mark.parametrize("val", [ From d01c456309bab17a2faf2e517937b9b9b5395602 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sun, 26 Oct 2025 16:12:33 +0000 Subject: [PATCH 2/5] fixing CI to install test deps --- .github/workflows/big_endian.yml | 2 +- .github/workflows/build_wheels.yml | 2 +- .github/workflows/ci.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/big_endian.yml b/.github/workflows/big_endian.yml index 02f4e7e2..9d85fdde 100644 --- a/.github/workflows/big_endian.yml +++ b/.github/workflows/big_endian.yml @@ -140,7 +140,7 @@ jobs: python -m pip install --break-system-packages --no-deps . -v --no-build-isolation --force-reinstall && # Install test dependencies separately - python -m pip install --break-system-packages pytest pytest-run-parallel pytest-timeout && + python -m pip install --break-system-packages pytest pytest-run-parallel pytest-timeout mpmath && cd .. python -m pytest -vvv --color=yes --timeout=600 --tb=short quaddtype/tests/ diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index 3db60f75..63918485 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -217,7 +217,7 @@ jobs: LDFLAGS: "-fopenmp" run: | python -m build --sdist --no-isolation --outdir dist/ - pip install --no-build-isolation dist/*.tar.gz -v + pip install --no-build-isolation dist/*.tar.gz[test] -v pytest -s tests working-directory: ./quaddtype diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 849f8f01..51d50e67 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,7 +65,7 @@ jobs: working-directory: quaddtype run: | export LDFLAGS="-fopenmp" - python -m pip install . -v --no-build-isolation + python -m pip install .[test] -v --no-build-isolation - name: Run quaddtype tests working-directory: quaddtype From d96860f3fe33bc608ea338a3a96e579142ae8475 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sun, 26 Oct 2025 16:16:57 +0000 Subject: [PATCH 3/5] fixing sdist job --- .github/workflows/build_wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index 63918485..b37beba0 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -217,7 +217,7 @@ jobs: LDFLAGS: "-fopenmp" run: | python -m build --sdist --no-isolation --outdir dist/ - pip install --no-build-isolation dist/*.tar.gz[test] -v + pip install --no-build-isolation "$(ls dist/*.tar.gz)"[test] -v pytest -s tests working-directory: ./quaddtype From 9ffa7ab36f91332346da04f6d6013f77a476b6ae Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sun, 26 Oct 2025 16:38:28 +0000 Subject: [PATCH 4/5] add attribute to stubs --- quaddtype/numpy_quaddtype/_quaddtype_main.pyi | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/quaddtype/numpy_quaddtype/_quaddtype_main.pyi b/quaddtype/numpy_quaddtype/_quaddtype_main.pyi index eab36b39..dfea0892 100644 --- a/quaddtype/numpy_quaddtype/_quaddtype_main.pyi +++ b/quaddtype/numpy_quaddtype/_quaddtype_main.pyi @@ -81,6 +81,12 @@ class QuadPrecision: # See https://github.com/python/mypy/issues/18343#issuecomment-2571784915 def __new__(cls, /, value: _IntoQuad, backend: _Backend = "sleef") -> Self: ... + # Attributes + @property + def real(self) -> Self: ... + @property + def imag(self) -> Self: ... + # Rich comparison operators # NOTE: Unlike other numpy scalars, these return `builtins.bool`, not `np.bool`. @override From b2ef947afa4747723370183c9b91b80f6bf15c4f Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sun, 26 Oct 2025 18:37:40 +0000 Subject: [PATCH 5/5] use nstr --- quaddtype/tests/test_quaddtype.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/quaddtype/tests/test_quaddtype.py b/quaddtype/tests/test_quaddtype.py index 6bc0c50d..7d396e97 100644 --- a/quaddtype/tests/test_quaddtype.py +++ b/quaddtype/tests/test_quaddtype.py @@ -1771,7 +1771,8 @@ def test_sin_cos_tan(self, op, val): quad_result = numpy_op(quad_val) mpmath_result = mpmath_op(mpf_val) # convert mpmath result to quad for comparison - mpmath_result = QuadPrecision(str(mpmath_result)) + # Use mp.nstr to get full precision (40 digits for quad precision) + mpmath_result = QuadPrecision(mp.nstr(mpmath_result, 40)) # Handle NaN cases if np.isnan(mpmath_result): @@ -1815,7 +1816,8 @@ def test_inverse_sin_cos(self, op, val): quad_result = numpy_op(quad_val) mpmath_result = mpmath_op(mpf_val) # convert mpmath result to quad for comparison - mpmath_result = QuadPrecision(str(mpmath_result)) + # Use mp.nstr to get full precision (40 digits for quad precision) + mpmath_result = QuadPrecision(mp.nstr(mpmath_result, 40)) # Handle NaN cases if np.isnan(mpmath_result): @@ -1941,7 +1943,8 @@ def test_atan2(self, y, x): # For all other cases, compare with mpmath mpmath_result = mp.atan2(mpf_y, mpf_x) - mpmath_result = QuadPrecision(str(mpmath_result)) + # Use mp.nstr to get full precision (40 digits for quad precision) + mpmath_result = QuadPrecision(mp.nstr(mpmath_result, 40)) if np.isnan(mpmath_result): assert np.isnan(quad_result), f"Expected NaN for atan2({y}, {x}), got {quad_result}"