Repository: ivy-llc/ivy

Issue:
Add `lgamma` to numpy backend
`Numpy` currently lacks a native implementation of the `lgamma` function, which computes the logarithm of the absolute value of "Gamma(x)" element-wise. Therefore, I propose adding this functionality to `numpy` using a compositional approach until a native implementation becomes available.


Discussion:
I can work on this if anyone has no objection?
@aadya940  Thanks for showing interest but I am currently working on it!

Resolving patch:
diff --git a/ivy/functional/backends/numpy/experimental/elementwise.py b/ivy/functional/backends/numpy/experimental/elementwise.py
--- a/ivy/functional/backends/numpy/experimental/elementwise.py
+++ b/ivy/functional/backends/numpy/experimental/elementwise.py
@@ -7,11 +7,6 @@
 from ivy.functional.backends.numpy.helpers import _scalar_output_to_0d_array
 from ivy.func_wrapper import with_unsupported_dtypes
 from . import backend_version
-from ivy.utils.exceptions import IvyNotImplementedException
-
-
-def lgamma(x: np.ndarray, /, *, out: Optional[np.ndarray] = None) -> np.ndarray:
-    raise IvyNotImplementedException()
 
 
 @_scalar_output_to_0d_array
@@ -304,3 +299,121 @@ def modf(
     out: Optional[np.ndarray] = None,
 ) -> np.ndarray:
     return np.modf(x, out=out)
+
+
+# --- LGAMMA --- #
+LANCZOS_N = 13
+lanczos_g = 6.024680040776729583740234375
+lanczos_num_coeffs = np.array(
+    [
+        23531376880.410759688572007674451636754734846804940,
+        42919803642.649098768957899047001988850926355848959,
+        35711959237.355668049440185451547166705960488635843,
+        17921034426.037209699919755754458931112671403265390,
+        6039542586.3520280050642916443072979210699388420708,
+        1439720407.3117216736632230727949123939715485786772,
+        248874557.86205415651146038641322942321632125127801,
+        31426415.585400194380614231628318205362874684987640,
+        2876370.6289353724412254090516208496135991145378768,
+        186056.26539522349504029498971604569928220784236328,
+        8071.6720023658162106380029022722506138218516325024,
+        210.82427775157934587250973392071336271166969580291,
+        2.5066282746310002701649081771338373386264310793408,
+    ]
+)
+lanczos_den_coeffs = np.array(
+    [
+        0.0,
+        39916800.0,
+        120543840.0,
+        150917976.0,
+        105258076.0,
+        45995730.0,
+        13339535.0,
+        2637558.0,
+        357423.0,
+        32670.0,
+        1925.0,
+        66.0,
+        1.0,
+    ]
+)
+
+
+def sinpi(x):
+    y = np.abs(x) % 2.0
+    n = np.round(2.0 * y)
+    assert 0 <= n and n <= 4
+
+    if n == 0:
+        r = np.sin(np.pi * y)
+    elif n == 1:
+        r = np.cos(np.pi * (y - 0.5))
+    elif n == 2:
+        r = np.sin(np.pi * (1.0 - y))
+    elif n == 3:
+        r = -np.cos(np.pi * (y - 1.5))
+    elif n == 4:
+        r = np.sin(np.pi * (y - 2.0))
+    else:
+        raise Exception("Unreachable code")
+
+    return np.copysign(1.0, x) * r
+
+
+def lanczos_sum(x):
+    num = 0.0
+    den = 0.0
+
+    if x < 5.0:
+        for i in range(LANCZOS_N - 1, -1, -1):
+            num = num * x + lanczos_num_coeffs[i]
+            den = den * x + lanczos_den_coeffs[i]
+    else:
+        for i in range(LANCZOS_N):
+            num = num / x + lanczos_num_coeffs[i]
+            den = den / x + lanczos_den_coeffs[i]
+
+    return num / den
+
+
+# TODO: Replace with native lgamma implementation when available
+def lgamma(
+    x: np.ndarray,
+    /,
+    *,
+    out: Optional[np.ndarray] = None,
+) -> np.ndarray:
+    def func(x):
+        if not np.isfinite(x):
+            if np.isnan(x):
+                return x  # lgamma(nan) = nan
+            else:
+                return np.inf  # lgamma(+-inf) = +inf
+
+        if x == np.floor(x) and x <= 2.0:
+            if x <= 0.0:
+                return np.inf  # lgamma(n) = inf for integers n <= 0
+            else:
+                return 0.0  # lgamma(1) = lgamma(2) = 0.0
+
+        absx = np.abs(x)
+        if absx < 1e-20:
+            return -np.log(absx)
+
+        # Lanczos' formula
+        r = np.log(lanczos_sum(absx)) - lanczos_g
+        r += (absx - 0.5) * (np.log(absx + lanczos_g - 0.5) - 1)
+
+        if x < 0.0:
+            # Use reflection formula to get value for negative x.
+            r = np.log(np.pi) - np.log(np.abs(sinpi(absx))) - np.log(absx) - r
+
+        if np.isinf(r):
+            raise OverflowError("Range error in lgamma")
+
+        return r
+
+    # Vectorize 'func' for element-wise operations on 'x', output matching 'x' dtype.
+    vfunc = np.vectorize(func, otypes=[x.dtype])
+    return vfunc(x)


Test patch: