Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/big_endian.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build_wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 "$(ls dist/*.tar.gz)"[test] -v
pytest -s tests
working-directory: ./quaddtype

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions quaddtype/numpy_quaddtype/_quaddtype_main.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member Author

@SwayamInSync SwayamInSync Oct 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These new attributes aren't added in stubs for the PR #208 , small addition do did in this PR

@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
Expand Down
1 change: 1 addition & 0 deletions quaddtype/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ dependencies = [
[project.optional-dependencies]
test = [
"pytest",
"mpmath",
"pytest-run-parallel"
]

Expand Down
14 changes: 7 additions & 7 deletions quaddtype/release_tracker.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | ✅ | ✅ |
Expand Down
219 changes: 219 additions & 0 deletions quaddtype/tests/test_quaddtype.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down Expand Up @@ -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", [
Expand Down
Loading