Skip to content

Commit 7b80182

Browse files
committed
trignometric tests
1 parent 0daf105 commit 7b80182

File tree

3 files changed

+227
-7
lines changed

3 files changed

+227
-7
lines changed

quaddtype/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ dependencies = [
3333
[project.optional-dependencies]
3434
test = [
3535
"pytest",
36+
"mpmath",
3637
"pytest-run-parallel"
3738
]
3839

quaddtype/release_tracker.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,13 @@
4040
| square |||
4141
| cbrt |||
4242
| reciprocal |||
43-
| sin || _Need: basic tests + edge cases (NaN/inf/0/π multiples/2π range)_ |
44-
| cos || _Need: basic tests + edge cases (NaN/inf/0/π multiples/2π range)_ |
45-
| tan || _Need: basic tests + edge cases (NaN/inf/0/π/2 asymptotes)_ |
46-
| arcsin || _Need: basic tests + edge cases (NaN/inf/±1/out-of-domain)_ |
47-
| arccos || _Need: basic tests + edge cases (NaN/inf/±1/out-of-domain)_ |
48-
| arctan || _Need: basic tests + edge cases (NaN/inf/0/asymptotes)_ |
49-
| arctan2 || _Need: basic tests + edge cases (NaN/inf/0/quadrant coverage)_ |
43+
| sin || |
44+
| cos || |
45+
| tan || |
46+
| arcsin || |
47+
| arccos || |
48+
| arctan || |
49+
| arctan2 || |
5050
| hypot |||
5151
| sinh |||
5252
| cosh |||

quaddtype/tests/test_quaddtype.py

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33
import numpy as np
44
import operator
55

6+
from mpmath import mp
7+
68
import numpy_quaddtype
79
from numpy_quaddtype import QuadPrecDType, QuadPrecision
10+
from numpy_quaddtype import pi as quad_pi
811

912

1013
def test_create_scalar_simple():
@@ -1735,6 +1738,222 @@ def test_divmod_broadcasting():
17351738
np.testing.assert_allclose(float(quotients[i]), expected_quotients[i], rtol=1e-14)
17361739
np.testing.assert_allclose(float(remainders[i]), expected_remainders[i], rtol=1e-14)
17371740

1741+
class TestTrignometricFunctions:
1742+
@pytest.mark.parametrize("op", ["sin", "cos", "tan", "atan"])
1743+
@pytest.mark.parametrize("val", [
1744+
# Basic cases
1745+
"0.0", "-0.0", "1.0", "-1.0", "2.0", "-2.0",
1746+
# pi multiples
1747+
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),
1748+
# Small values
1749+
"1e-10", "-1e-10", "1e-15", "-1e-15",
1750+
# Values near one
1751+
"0.9", "-0.9", "0.9999", "-0.9999",
1752+
"1.1", "-1.1", "1.0001", "-1.0001",
1753+
# Medium values
1754+
"10.0", "-10.0", "20.0", "-20.0",
1755+
# Large values
1756+
"100.0", "200.0", "700.0", "1000.0", "1e100", "1e308",
1757+
"-100.0", "-200.0", "-700.0", "-1000.0", "-1e100", "-1e308",
1758+
# Fractional values
1759+
"0.5", "-0.5", "1.5", "-1.5", "2.5", "-2.5",
1760+
# Special values
1761+
"inf", "-inf", "nan",
1762+
])
1763+
def test_sin_cos_tan(self, op, val):
1764+
mp.prec = 113 # Set precision to 113 bits (~34 decimal digits)
1765+
numpy_op = getattr(np, op)
1766+
mpmath_op = getattr(mp, op)
1767+
1768+
quad_val = QuadPrecision(val)
1769+
mpf_val = mp.mpf(val)
1770+
1771+
quad_result = numpy_op(quad_val)
1772+
mpmath_result = mpmath_op(mpf_val)
1773+
# convert mpmath result to quad for comparison
1774+
mpmath_result = QuadPrecision(str(mpmath_result))
1775+
1776+
# Handle NaN cases
1777+
if np.isnan(mpmath_result):
1778+
assert np.isnan(quad_result), f"Expected NaN for {op}({val}), got {quad_result}"
1779+
return
1780+
1781+
# Handle infinity cases
1782+
if np.isinf(mpmath_result):
1783+
assert np.isinf(quad_result), f"Expected inf for {op}({val}), got {quad_result}"
1784+
assert np.sign(mpmath_result) == np.sign(quad_result), f"Infinity sign mismatch for {op}({val})"
1785+
return
1786+
1787+
# For finite non-zero results
1788+
np.testing.assert_allclose(quad_result, mpmath_result, rtol=1e-32, atol=1e-34,
1789+
err_msg=f"Value mismatch for {op}({val}), expected {mpmath_result}, got {quad_result}")
1790+
1791+
# their domain is [-1 , 1]
1792+
@pytest.mark.parametrize("op", ["asin", "acos"])
1793+
@pytest.mark.parametrize("val", [
1794+
# Basic cases (valid domain)
1795+
"0.0", "-0.0", "1.0", "-1.0",
1796+
# Small values
1797+
"1e-10", "-1e-10", "1e-15", "-1e-15",
1798+
# Values near domain boundaries
1799+
"0.9", "-0.9", "0.9999", "-0.9999",
1800+
"0.99999999", "-0.99999999",
1801+
"0.999999999999", "-0.999999999999",
1802+
# Fractional values (within domain)
1803+
"0.5", "-0.5",
1804+
# Special values
1805+
"nan"
1806+
])
1807+
def test_inverse_sin_cos(self, op, val):
1808+
mp.prec = 113 # Set precision to 113 bits (~34 decimal digits)
1809+
numpy_op = getattr(np, op)
1810+
mpmath_op = getattr(mp, op)
1811+
1812+
quad_val = QuadPrecision(val)
1813+
mpf_val = mp.mpf(val)
1814+
1815+
quad_result = numpy_op(quad_val)
1816+
mpmath_result = mpmath_op(mpf_val)
1817+
# convert mpmath result to quad for comparison
1818+
mpmath_result = QuadPrecision(str(mpmath_result))
1819+
1820+
# Handle NaN cases
1821+
if np.isnan(mpmath_result):
1822+
assert np.isnan(quad_result), f"Expected NaN for {op}({val}), got {quad_result}"
1823+
return
1824+
1825+
# For finite non-zero results
1826+
np.testing.assert_allclose(quad_result, mpmath_result, rtol=1e-32, atol=1e-34,
1827+
err_msg=f"Value mismatch for {op}({val}), expected {mpmath_result}, got {quad_result}")
1828+
1829+
# mpmath's atan2 does not follow IEEE standards so hardcoding the edge cases
1830+
# for special edge cases check reference here: https://en.cppreference.com/w/cpp/numeric/math/atan2.html
1831+
# atan2: [Real x Real] -> [-pi , pi]
1832+
@pytest.mark.parametrize("y", [
1833+
# Basic cases
1834+
"0.0", "-0.0", "1.0", "-1.0",
1835+
# Small values
1836+
"1e-10", "-1e-10", "1e-15", "-1e-15",
1837+
# Medium/Large values
1838+
"10.0", "-10.0", "100.0", "-100.0", "1000.0", "-1000.0",
1839+
# Fractional
1840+
"0.5", "-0.5", "2.5", "-2.5",
1841+
# Special
1842+
"inf", "-inf", "nan",
1843+
])
1844+
@pytest.mark.parametrize("x", [
1845+
"0.0", "-0.0", "1.0", "-1.0",
1846+
"1e-10", "-1e-10",
1847+
"10.0", "-10.0", "100.0", "-100.0",
1848+
"0.5", "-0.5",
1849+
"inf", "-inf", "nan",
1850+
])
1851+
def test_atan2(self, y, x):
1852+
mp.prec = 113
1853+
1854+
quad_y = QuadPrecision(y)
1855+
quad_x = QuadPrecision(x)
1856+
mpf_y = mp.mpf(y)
1857+
mpf_x = mp.mpf(x)
1858+
1859+
quad_result = np.arctan2(quad_y, quad_x)
1860+
1861+
# IEEE 754 special cases - hardcoded expectations
1862+
y_val = float(y)
1863+
x_val = float(x)
1864+
1865+
# If either x is NaN or y is NaN, NaN is returned
1866+
if np.isnan(y_val) or np.isnan(x_val):
1867+
assert np.isnan(quad_result), f"Expected NaN for atan2({y}, {x}), got {quad_result}"
1868+
return
1869+
1870+
# If y is ±0 and x is negative or -0, ±π is returned
1871+
if y_val == 0.0 and (x_val < 0.0 or (x_val == 0.0 and np.signbit(x_val))):
1872+
expected = quad_pi if not np.signbit(y_val) else -quad_pi
1873+
np.testing.assert_allclose(quad_result, expected, rtol=1e-32, atol=1e-34,
1874+
err_msg=f"Value mismatch for atan2({y}, {x}), expected {expected}, got {quad_result}")
1875+
return
1876+
1877+
# If y is ±0 and x is positive or +0, ±0 is returned
1878+
if y_val == 0.0 and (x_val > 0.0 or (x_val == 0.0 and not np.signbit(x_val))):
1879+
assert quad_result == 0.0, f"Expected ±0 for atan2({y}, {x}), got {quad_result}"
1880+
assert np.signbit(quad_result) == np.signbit(y_val), f"Sign mismatch for atan2({y}, {x})"
1881+
return
1882+
1883+
# If y is ±∞ and x is finite, ±π/2 is returned
1884+
if np.isinf(y_val) and np.isfinite(x_val):
1885+
expected = quad_pi / 2 if y_val > 0 else -quad_pi / 2
1886+
np.testing.assert_allclose(quad_result, expected, rtol=1e-32, atol=1e-34,
1887+
err_msg=f"Value mismatch for atan2({y}, {x}), expected {expected}, got {quad_result}")
1888+
return
1889+
1890+
# If y is ±∞ and x is -∞, ±3π/4 is returned
1891+
if np.isinf(y_val) and np.isinf(x_val) and x_val < 0:
1892+
expected = 3 * quad_pi / 4 if y_val > 0 else -3 * quad_pi / 4
1893+
np.testing.assert_allclose(quad_result, expected, rtol=1e-32, atol=1e-34,
1894+
err_msg=f"Value mismatch for atan2({y}, {x}), expected {expected}, got {quad_result}")
1895+
return
1896+
1897+
# If y is ±∞ and x is +∞, ±π/4 is returned
1898+
if np.isinf(y_val) and np.isinf(x_val) and x_val > 0:
1899+
expected = quad_pi / 4 if y_val > 0 else -quad_pi / 4
1900+
np.testing.assert_allclose(quad_result, expected, rtol=1e-32, atol=1e-34,
1901+
err_msg=f"Value mismatch for atan2({y}, {x}), expected {expected}, got {quad_result}")
1902+
return
1903+
1904+
# If x is ±0 and y is negative, -π/2 is returned
1905+
if x_val == 0.0 and y_val < 0.0:
1906+
expected = -quad_pi / 2
1907+
np.testing.assert_allclose(quad_result, expected, rtol=1e-32, atol=1e-34,
1908+
err_msg=f"Value mismatch for atan2({y}, {x}), expected {expected}, got {quad_result}")
1909+
return
1910+
1911+
# If x is ±0 and y is positive, +π/2 is returned
1912+
if x_val == 0.0 and y_val > 0.0:
1913+
expected = quad_pi / 2
1914+
np.testing.assert_allclose(quad_result, expected, rtol=1e-32, atol=1e-34,
1915+
err_msg=f"Value mismatch for atan2({y}, {x}), expected {expected}, got {quad_result}")
1916+
return
1917+
1918+
# If x is -∞ and y is finite and positive, +π is returned
1919+
if np.isinf(x_val) and x_val < 0 and np.isfinite(y_val) and y_val > 0.0:
1920+
expected = quad_pi
1921+
np.testing.assert_allclose(quad_result, expected, rtol=1e-32, atol=1e-34,
1922+
err_msg=f"Value mismatch for atan2({y}, {x}), expected {expected}, got {quad_result}")
1923+
return
1924+
1925+
# If x is -∞ and y is finite and negative, -π is returned
1926+
if np.isinf(x_val) and x_val < 0 and np.isfinite(y_val) and y_val < 0.0:
1927+
expected = -quad_pi
1928+
np.testing.assert_allclose(quad_result, expected, rtol=1e-32, atol=1e-34,
1929+
err_msg=f"Value mismatch for atan2({y}, {x}), expected {expected}, got {quad_result}")
1930+
return
1931+
1932+
# If x is +∞ and y is finite and positive, +0 is returned
1933+
if np.isinf(x_val) and x_val > 0 and np.isfinite(y_val) and y_val > 0.0:
1934+
assert quad_result == 0.0 and not np.signbit(quad_result), f"Expected +0 for atan2({y}, {x}), got {quad_result}"
1935+
return
1936+
1937+
# If x is +∞ and y is finite and negative, -0 is returned
1938+
if np.isinf(x_val) and x_val > 0 and np.isfinite(y_val) and y_val < 0.0:
1939+
assert quad_result == 0.0 and np.signbit(quad_result), f"Expected -0 for atan2({y}, {x}), got {quad_result}"
1940+
return
1941+
1942+
# For all other cases, compare with mpmath
1943+
mpmath_result = mp.atan2(mpf_y, mpf_x)
1944+
mpmath_result = QuadPrecision(str(mpmath_result))
1945+
1946+
if np.isnan(mpmath_result):
1947+
assert np.isnan(quad_result), f"Expected NaN for atan2({y}, {x}), got {quad_result}"
1948+
return
1949+
1950+
if np.isinf(mpmath_result):
1951+
assert np.isinf(quad_result), f"Expected inf for atan2({y}, {x}), got {quad_result}"
1952+
assert np.sign(mpmath_result) == np.sign(quad_result), f"Infinity sign mismatch for atan2({y}, {x})"
1953+
return
1954+
1955+
np.testing.assert_allclose(quad_result, mpmath_result, rtol=1e-32, atol=1e-34,
1956+
err_msg=f"Value mismatch for atan2({y}, {x}), expected {mpmath_result}, got {quad_result}")
17381957

17391958
@pytest.mark.parametrize("op", ["sinh", "cosh", "tanh", "arcsinh", "arccosh", "arctanh"])
17401959
@pytest.mark.parametrize("val", [

0 commit comments

Comments
 (0)