Skip to content

Commit 4843ffb

Browse files
authored
Add numerically sound implementations for log1p and expm1 (#234)
1 parent 01649da commit 4843ffb

5 files changed

Lines changed: 66 additions & 21 deletions

File tree

CHANGELOG.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@
77
Changelog
88
=========
99

10+
0.22.0 (unreleased)
11+
-------------------
12+
13+
**New feature**
14+
15+
- Added support for :func:`ndonnx.log1p` and :func:`ndonnx.expm1`.
16+
17+
1018
0.21.0 (2026-08-07)
1119
-------------------
1220

ndonnx/_funcs.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -449,15 +449,11 @@ def expand_dims(x: Array, /, *, axis: int = 0) -> Array:
449449

450450

451451
def expm1(x: Array, /) -> Array:
452-
# Requires special operator to meet standards precision requirements
453-
# TODO: Add upstream tracking issue
454-
raise NotImplementedError
452+
return Array._from_tyarray(x._tyarray.expm1())
455453

456454

457455
def log1p(x: Array, /) -> Array:
458-
# Requires special operator to meet standards precision requirements
459-
# TODO: Add upstream tracking issue
460-
raise NotImplementedError
456+
return Array._from_tyarray(x._tyarray.log1p())
461457

462458

463459
def conj(x: Array, /) -> Array:

ndonnx/_typed_array/masked_onnx.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -755,7 +755,9 @@ def dtype(self) -> FloatDTypes:
755755
cos = _make_unary_member_same_type("cos") # type: ignore
756756
cosh = _make_unary_member_same_type("cosh") # type: ignore
757757
exp = _make_unary_member_same_type("exp") # type: ignore
758+
expm1 = _make_unary_member_same_type("expm1") # type: ignore
758759
log = _make_unary_member_same_type("log") # type: ignore
760+
log1p = _make_unary_member_same_type("log1p") # type: ignore
759761
log2 = _make_unary_member_same_type("log2") # type: ignore
760762
log10 = _make_unary_member_same_type("log10") # type: ignore
761763
sin = _make_unary_member_same_type("sin") # type: ignore

ndonnx/_typed_array/onnx.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1928,6 +1928,38 @@ def __ndx_rlogaddexp__(self, x1: TyArrayBase | int | float, /) -> TyArrayFloatin
19281928
return safe_cast(TyArrayFloating, (x1.exp() + x2.exp()).log())
19291929
return NotImplemented
19301930

1931+
@overload
1932+
def __add__(self: Self, other: Self | int | float) -> Self: ...
1933+
@overload
1934+
def __add__(self, other: TyArrayNumber | int | float) -> TyArrayNumber: ...
1935+
@overload
1936+
def __add__(self, other: TyArrayBase | PyScalar) -> TyArrayBase: ...
1937+
def __add__(self, other: TyArrayBase | PyScalar) -> TyArrayBase:
1938+
return super().__add__(other)
1939+
1940+
@overload
1941+
def __sub__(self: Self, other: Self | int | float) -> Self: ...
1942+
@overload
1943+
def __sub__(self, other: TyArrayNumber | int | float) -> TyArrayNumber: ...
1944+
@overload
1945+
def __sub__(self, other: TyArrayBase | PyScalar) -> TyArrayBase: ...
1946+
def __sub__(self, other: TyArrayBase | PyScalar) -> TyArrayBase:
1947+
return super().__sub__(other)
1948+
1949+
@overload
1950+
def __mul__(self: Self, other: Self | int | float) -> Self: ...
1951+
@overload
1952+
def __mul__(self, other: TyArrayBase | PyScalar) -> TyArrayBase: ...
1953+
def __mul__(self, other: TyArrayBase | PyScalar) -> TyArrayBase:
1954+
return super().__mul__(other)
1955+
1956+
@overload
1957+
def __truediv__(self: Self, other: Self | int | float) -> Self: ...
1958+
@overload
1959+
def __truediv__(self, other: TyArrayBase | PyScalar) -> TyArrayBase: ...
1960+
def __truediv__(self, other: TyArrayBase | PyScalar) -> TyArrayBase:
1961+
return super().__truediv__(other)
1962+
19311963
def ceil(self) -> Self:
19321964
return type(self)(op.ceil(self._var))
19331965

@@ -2073,6 +2105,14 @@ def cosh(self) -> Self:
20732105
def exp(self) -> Self:
20742106
return type(self)(op.exp(self._var))
20752107

2108+
def expm1(self) -> Self:
2109+
# expm1(x) = (u - 1) * x / log(u) with u = exp(x); == x where u == 1.
2110+
# Analog of Goldberg's log1p (see below).
2111+
u = self.exp()
2112+
d = u - 1.0
2113+
tail = (d == -1.0) | u.isinf()
2114+
return where(u == 1.0, self, where(tail, d, d * self / u.log()))
2115+
20762116
def log(self) -> Self:
20772117
return type(self)(op.log(self._var))
20782118

@@ -2084,6 +2124,16 @@ def log10(self) -> Self:
20842124
res = self.log() / float(np.log(10))
20852125
return safe_cast(type(self), res)
20862126

2127+
def log1p(self) -> Self:
2128+
# log1p(x) = log(u) * x / (u - 1) with u = 1 + x; == x where u == 1.
2129+
# Goldberg, "What Every Computer Scientist Should Know About
2130+
# Floating-Point Arithmetic", Theorem 4:
2131+
# https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
2132+
u = self + 1.0
2133+
d = u - 1.0
2134+
short_circuit = (u == 1.0) | (self.isinf() & (self > 0.0))
2135+
return where(short_circuit, self, u.log() * (self / d))
2136+
20872137
def sin(self) -> Self:
20882138
return type(self)(op.sin(self._var))
20892139

skips.txt

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,11 @@ array_api_tests/test_has_names.py::test_has_names[elementwise-imag]
3737
array_api_tests/test_has_names.py::test_has_names[elementwise-real]
3838
array_api_tests/test_has_names.py::test_has_names[elementwise-signbit]
3939
array_api_tests/test_has_names.py::test_has_names[elementwise-atan2]
40-
array_api_tests/test_has_names.py::test_has_names[elementwise-expm1]
41-
array_api_tests/test_has_names.py::test_has_names[elementwise-log1p]
4240
array_api_tests/test_has_names.py::test_has_names[elementwise-nextafter]
4341

4442
array_api_tests/test_operators_and_elementwise_functions.py::test_atan2
4543
array_api_tests/test_operators_and_elementwise_functions.py::test_copysign
46-
array_api_tests/test_operators_and_elementwise_functions.py::test_expm1
4744
array_api_tests/test_operators_and_elementwise_functions.py::test_hypot
48-
array_api_tests/test_operators_and_elementwise_functions.py::test_log1p
4945
array_api_tests/test_operators_and_elementwise_functions.py::test_signbit
5046
array_api_tests/test_operators_and_elementwise_functions.py::test_nextafter
5147

@@ -85,24 +81,17 @@ array_api_tests/test_special_cases.py::test_binary[nextafter(x1_i is NaN or x2_i
8581
array_api_tests/test_special_cases.py::test_binary[nextafter(x1_i is -0 and x2_i is +0) -> +0]
8682
array_api_tests/test_special_cases.py::test_binary[nextafter(x1_i is +0 and x2_i is -0) -> -0]
8783

84+
# log1p and expm1 fail to propagate -0 because onnxruntime's where fails to do so
85+
array_api_tests/test_special_cases.py::test_unary[log1p(x_i is -0) -> -0]
86+
array_api_tests/test_special_cases.py::test_unary[expm1(x_i is -0) -> -0]
87+
8888
array_api_tests/test_special_cases.py::test_unary[acos(x_i < -1) -> NaN]
8989
array_api_tests/test_special_cases.py::test_unary[acos(x_i > 1) -> NaN]
9090
array_api_tests/test_special_cases.py::test_unary[acosh(x_i < 1) -> NaN]
9191
array_api_tests/test_special_cases.py::test_unary[asin(x_i < -1) -> NaN]
9292
array_api_tests/test_special_cases.py::test_unary[asin(x_i > 1) -> NaN]
9393
array_api_tests/test_special_cases.py::test_unary[atanh(x_i < -1) -> NaN]
9494
array_api_tests/test_special_cases.py::test_unary[atanh(x_i > 1) -> NaN]
95-
array_api_tests/test_special_cases.py::test_unary[expm1(x_i is +0) -> +0]
96-
array_api_tests/test_special_cases.py::test_unary[expm1(x_i is +infinity) -> +infinity]
97-
array_api_tests/test_special_cases.py::test_unary[expm1(x_i is -0) -> -0]
98-
array_api_tests/test_special_cases.py::test_unary[expm1(x_i is -infinity) -> -1]
99-
array_api_tests/test_special_cases.py::test_unary[expm1(x_i is NaN) -> NaN]
100-
array_api_tests/test_special_cases.py::test_unary[log1p(x_i < -1) -> NaN]
101-
array_api_tests/test_special_cases.py::test_unary[log1p(x_i is +0) -> +0]
102-
array_api_tests/test_special_cases.py::test_unary[log1p(x_i is +infinity) -> +infinity]
103-
array_api_tests/test_special_cases.py::test_unary[log1p(x_i is -0) -> -0]
104-
array_api_tests/test_special_cases.py::test_unary[log1p(x_i is -1) -> -infinity]
105-
array_api_tests/test_special_cases.py::test_unary[log1p(x_i is NaN) -> NaN]
10695
array_api_tests/test_special_cases.py::test_unary[signbit(isfinite(x_i) and x_i < 0) -> True]
10796
array_api_tests/test_special_cases.py::test_unary[signbit(isfinite(x_i) and x_i > 0) -> False]
10897
array_api_tests/test_special_cases.py::test_unary[signbit(x_i is +0) -> False]

0 commit comments

Comments
 (0)