├── MANIFEST.in ├── requirements.txt ├── setup.py ├── README.md ├── LICENSE ├── .gitignore ├── tests └── test_autograd_gamma.py └── autograd_gamma └── __init__.py /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | autograd>=1.2.0 2 | scipy>=1.2.0 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import setuptools 4 | 5 | with open("README.md", "r") as fh: 6 | long_description = fh.read() 7 | 8 | setuptools.setup( 9 | name="autograd-gamma", 10 | version="0.5.0", 11 | description="Autograd compatible approximations to the gamma family of functions", 12 | license='MIT License', 13 | author="Cameron Davidson-Pilon", 14 | author_email="cam.davidson.pilon@gmail.com", 15 | url="https://github.com/CamDavidsonPilon/autograd-gamma", 16 | keywords=["autograd", "gamma", "incomplete gamma function"], 17 | long_description=long_description, 18 | long_description_content_type="text/markdown", 19 | classifiers=[ 20 | "Intended Audience :: Science/Research", 21 | "Topic :: Scientific/Engineering", 22 | "Programming Language :: Python :: 2", 23 | "Programming Language :: Python :: 3", 24 | ], 25 | install_requires=["autograd>=1.2.0", "scipy>=1.2.0"], 26 | packages=setuptools.find_packages(), 27 | ) 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # autograd-gamma 2 | [![PyPI version](https://badge.fury.io/py/autograd-gamma.svg)](https://badge.fury.io/py/autograd-gamma) 3 | 4 | 5 | [autograd](https://github.com/HIPS/autograd) compatible approximations to the derivatives of the Gamma-family of functions. 6 | 7 | 8 | # Tutorial 9 | 10 | ```python 11 | from autograd import grad 12 | from autograd_gamma import gammainc, gammaincc, gammaincln, gammainccln 13 | 14 | 15 | grad(gammainc, argnum=0)(1., 2.) 16 | grad(gammaincc, argnum=0)(1., 2.) 17 | 18 | # logarithmic functions too. 19 | grad(gammaincln, argnum=0)(1., 2.) 20 | grad(gammainccln, argnum=0)(1., 2.) 21 | 22 | 23 | 24 | from autograd_gamma import betainc, betaincln 25 | 26 | grad(betainc, argnum=0)(1., 2., 0.5) 27 | grad(betainc, argnum=1)(1., 2., 0.5) 28 | 29 | # logarithmic functions too. 30 | grad(betaincln, argnum=0)(1., 2., 0.5) 31 | grad(betaincln, argnum=1)(1., 2., 0.5) 32 | 33 | ``` 34 | 35 | 36 | # Long-term goal 37 | 38 | Build and improve upon the derivative of the upper and lower incomplete gamma functions. Eventually, if we have a fast analytical solution, we will merge into the autograd library. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Cameron Davidson-Pilon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /tests/test_autograd_gamma.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from autograd import grad, jacobian 3 | from autograd.scipy.special import ( 4 | gammainc as scipy_gammainc, 5 | gammaincc as scipy_gammaincc, 6 | gamma, 7 | ) 8 | from autograd_gamma import gammainc, gammaincc, betainc, gammaincln, gammaincinv, gammainccinv, betaincinv 9 | import numpy as np 10 | import numpy.testing as npt 11 | from scipy.special import expi 12 | from scipy.optimize import check_grad 13 | 14 | 15 | def test_inc_gamma_second_argument(): 16 | for a in np.logspace(-5, 2): 17 | for x in np.logspace(-5, 2): 18 | npt.assert_allclose( 19 | grad(gammainc, argnum=1)(a, x), grad(scipy_gammainc, argnum=1)(a, x) 20 | ) 21 | npt.assert_allclose( 22 | grad(gammaincc, argnum=1)(a, x), grad(scipy_gammaincc, argnum=1)(a, x) 23 | ) 24 | 25 | def test_log_gamma(): 26 | gammaincln(1., 1.) 27 | grad(gammaincln)(1., 1.) 28 | grad(gammaincln, argnum=1)(1., 1.) 29 | 30 | 31 | def test_a_special_case_of_the_derivative(): 32 | """ 33 | We know a specific to test against: 34 | 35 | dUIG(s, x) / ds at (s=1, x) = ln(x) * UIG(1, x) + E_1(x) 36 | 37 | where E_1(x) is the exponential integral 38 | """ 39 | 40 | # incomplete upper gamma 41 | IUG = lambda s, x: gammaincc(s, x) * gamma(s) 42 | 43 | def analytical_derivative(x): 44 | dIUG = np.log(x) * IUG(1.0, x) - expi(-x) 45 | return dIUG 46 | 47 | def approx_derivative(x): 48 | return jacobian(IUG, argnum=0)(1.0, x) 49 | 50 | x = np.linspace(1, 12) 51 | npt.assert_allclose(analytical_derivative(x), approx_derivative(x)) 52 | 53 | x = np.logspace(-25, 25, 100) 54 | npt.assert_allclose(analytical_derivative(x), approx_derivative(x)) 55 | 56 | 57 | def test_large_x(): 58 | npt.assert_allclose(grad(gammainc, argnum=1)(100.0, 10000.0), 0) 59 | npt.assert_allclose(grad(gammaincc, argnum=1)(100.0, 10000.0), 0) 60 | 61 | 62 | def test_betainc_to_known_values(): 63 | 64 | d_beta_inc_0 = jacobian(betainc, argnum=0) 65 | 66 | # I_0(a, b) = 0 67 | npt.assert_allclose(d_beta_inc_0(0.3, 1.0, 0), 0) 68 | 69 | # I_1(a, b) = 1 70 | npt.assert_allclose(d_beta_inc_0(0.3, 1.0, 1), 0) 71 | 72 | # I_x(a, 1) = x ** a 73 | x = np.linspace(0, 1) 74 | a = np.linspace(0.1, 10) 75 | npt.assert_allclose(d_beta_inc_0(a, 1.0, x), jacobian(lambda a, x: x ** a)(a, x)) 76 | 77 | d_beta_inc_1 = jacobian(betainc, argnum=1) 78 | 79 | # I_0(a, b) = 0 80 | npt.assert_allclose(d_beta_inc_1(0.3, 1.0, 0), 0) 81 | 82 | # I_1(a, b) = 1 83 | npt.assert_allclose(d_beta_inc_1(0.3, 1.0, 1), 0) 84 | 85 | x = np.linspace(0.001, 0.5) 86 | b = np.linspace(0.1, 2.0) 87 | # I_x(1, b) = 1 - (1-x) ** b 88 | npt.assert_allclose( 89 | d_beta_inc_1(1.0, b, x), jacobian(lambda b, x: 1 - (1 - x) ** b)(b, x) 90 | ) 91 | 92 | 93 | 94 | def test_gammainc(): 95 | 96 | for a in np.logspace(-0.1, 2, 10): 97 | gammainc_1 = lambda x: gammainc(a, x) 98 | gammainc_2 = lambda x: grad(gammainc, argnum=1)(a, x) 99 | 100 | assert check_grad(gammainc_1, gammainc_2, 1e-4) < 0.0001 101 | assert check_grad(gammainc_1, gammainc_2, 1e-3) < 0.0001 102 | assert check_grad(gammainc_1, gammainc_2, 1e-2) < 0.0001 103 | assert check_grad(gammainc_1, gammainc_2, 1e-1) < 0.0001 104 | assert check_grad(gammainc_1, gammainc_2, 1e-0) < 0.0001 105 | assert check_grad(gammainc_1, gammainc_2, 1e1) < 0.0001 106 | assert check_grad(gammainc_1, gammainc_2, 1e2) < 0.0001 107 | 108 | def test_gammaincinv(): 109 | for a in np.logspace(-1, 1, 10): 110 | for y in np.linspace(0.001, 0.99, 10): 111 | gammaincinv_1 = lambda x: gammaincinv(a, x) 112 | gammaincinv_2 = lambda x: grad(gammaincinv, argnum=1)(a, x) 113 | assert check_grad(gammaincinv_1, gammaincinv_2, y) < 0.01, (a, y) 114 | 115 | def test_gammainccinv(): 116 | for a in np.logspace(-1, 1, 10): 117 | for y in np.linspace(0.01, 0.99, 10): 118 | gammainccinv_1 = lambda x: gammainccinv(a, x) 119 | gammainccinv_2 = lambda x: grad(gammainccinv, argnum=1)(a, x) 120 | assert check_grad(gammainccinv_1, gammainccinv_2, y) < 0.0005, (a, y) 121 | 122 | def test_betaincinv(): 123 | for a in np.logspace(-1, 2, 10): 124 | for b in np.logspace(-1, 2, 10): 125 | for y in np.linspace(0.01, 0.99, 10): 126 | betaincinv_1 = lambda x: betaincinv(a, b, x) 127 | betaincinv_2 = lambda x: grad(betaincinv, argnum=2)(a, b, x) 128 | assert check_grad(betaincinv_1, betaincinv_2, y) < 0.0005, (a, b, y) 129 | 130 | 131 | 132 | @pytest.mark.xfail 133 | def test_gammainc_fails(): 134 | a = 0.1 135 | gammainc_1 = lambda x: gammainc(a, x) 136 | gammainc_2 = lambda x: grad(gammainc, argnum=1)(a, x) 137 | assert not check_grad(gammainc_1, gammainc_2, 1e-4) < 0.0001 138 | 139 | -------------------------------------------------------------------------------- /autograd_gamma/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from autograd.extend import primitive, defvjp 3 | from autograd import numpy as np 4 | from autograd.scipy.special import gammaln, beta 5 | from autograd.numpy.numpy_vjps import unbroadcast_f 6 | from scipy.special import ( 7 | gammainc as _scipy_gammainc, 8 | gammaincc as _scipy_gammaincc, 9 | gamma, 10 | betainc as _scipy_betainc, 11 | gammainccinv as _scipy_gammainccinv, 12 | gammaincinv as _scipy_gammaincinv, 13 | betaincinv as _scipy_betaincinv, 14 | ) 15 | 16 | __all__ = [ 17 | "gamma", # gamma function 18 | "gammainc", # regularized lower incomplete gamma function 19 | "gammaincln", # log of regularized lower incomplete gamma function 20 | "gammaincc", # regularized upper incomplete gamma function 21 | "gammainccln", # log of regularized upper incomplete gamma function 22 | "beta", # beta function 23 | "betainc", # incomplete beta function 24 | "betaincln", # log of incomplete beta function 25 | 'gammaincinv', # inverse of the gammainc w.r.t. the second argument, solves y = P(a, x) 26 | 'gammainccinv', # inverse of the gammaincc w.r.t. the second argument, solves y = Q(a, x) 27 | 'betaincinv', # inverse of the betainc, solves y = B(a, b, x) 28 | ] 29 | 30 | 31 | LOG_EPISILON = 1e-35 32 | MACHINE_EPISLON_POWER = np.finfo(float).eps ** (1 / 3) 33 | 34 | gammainc = primitive(_scipy_gammainc) 35 | gammaincc = primitive(_scipy_gammaincc) 36 | betainc = primitive(_scipy_betainc) 37 | gammainccinv = primitive(_scipy_gammainccinv) 38 | gammaincinv = primitive(_scipy_gammaincinv) 39 | betaincinv = primitive(_scipy_betaincinv) 40 | 41 | 42 | @primitive 43 | def gammainccln(a, x): 44 | return np.log(np.clip(gammaincc(a, x), LOG_EPISILON, 1 - LOG_EPISILON)) 45 | 46 | 47 | @primitive 48 | def gammaincln(a, x): 49 | return np.log(np.clip(gammainc(a, x), LOG_EPISILON, 1 - LOG_EPISILON)) 50 | 51 | 52 | @primitive 53 | def betaincln(a, b, x): 54 | return np.log(np.clip(betainc(a, b, x), LOG_EPISILON, 1 - LOG_EPISILON)) 55 | 56 | 57 | def central_difference_of_(f, argnum=0): 58 | """ 59 | 5th order approximation. 60 | """ 61 | new_f = lambda x, *args: f(*args[:argnum], x, *args[argnum:]) 62 | 63 | def _central_difference(_, *args): 64 | x = args[argnum] 65 | args = args[:argnum] + args[argnum + 1 :] 66 | 67 | # Why do we calculate a * MACHINE_EPSILON_POWER? 68 | # consider if x is massive, like, 2**100. Then even for a simple 69 | # function like the identity function, (2**100 + h) - 2**100 = 0 due 70 | # to floating points. (the correct answer should be 1.0) 71 | delta = np.maximum(x * MACHINE_EPISLON_POWER, 1e-7) 72 | 73 | # another thing to consider is that x is machine representable, but x + h is 74 | # rarely, and will be rounded to be machine representable. This (x + h) - x != h. 75 | temp = x + delta 76 | delta = temp - x 77 | return unbroadcast_f( 78 | x, 79 | lambda g: g 80 | * ( 81 | -1 * new_f(x + 2 * delta, *args) 82 | + 8 * new_f(x + delta, *args) 83 | - 8 * new_f(x - delta, *args) 84 | + 1 * new_f(x - 2 * delta, *args) 85 | ) 86 | / (12 * delta), 87 | ) 88 | 89 | return _central_difference 90 | 91 | 92 | def central_difference_of_log(f, argnum=0): 93 | """ 94 | 5th order approximation of derivative of log(f). We take advantage of the fact: 95 | 96 | d(log(f))/dx = 1/f df/dx 97 | 98 | So we approximate the second term only. 99 | """ 100 | new_f = lambda x, *args: f(*args[:argnum], x, *args[argnum:]) 101 | 102 | def _central_difference(_, *args): 103 | x = args[argnum] 104 | args = args[:argnum] + args[argnum + 1 :] 105 | 106 | # Why do we calculate a * MACHINE_EPSILON_POWER? 107 | # consider if x is massive, like, 2**100. Then even for a simple 108 | # function like the identity function, ((2**100 + h) - 2**100)/h = 0 due 109 | # to floating points. (the correct answer should be 1.0) 110 | delta = np.maximum(x * MACHINE_EPISLON_POWER, 1e-7) 111 | 112 | # another thing to consider is that x is machine representable, but x + h is 113 | # rarely, and will be rounded to be machine representable. This (x + h) - x != h. 114 | temp = x + delta 115 | delta = temp - x 116 | return unbroadcast_f( 117 | x, 118 | lambda g: g 119 | * ( 120 | -1 * new_f(x + 2 * delta, *args) 121 | + 8 * new_f(x + 1 * delta, *args) 122 | - 8 * new_f(x - 1 * delta, *args) 123 | + 1 * new_f(x - 2 * delta, *args) 124 | ) 125 | / (12 * delta) 126 | / new_f(x, *args), 127 | ) 128 | 129 | return _central_difference 130 | 131 | 132 | defvjp( 133 | gammainc, 134 | central_difference_of_(gammainc), 135 | lambda ans, a, x: unbroadcast_f( 136 | x, lambda g: g * np.exp(-x + np.log(x) * (a - 1) - gammaln(a)) 137 | ), 138 | ) 139 | 140 | defvjp( 141 | gammaincc, 142 | central_difference_of_(gammaincc), 143 | lambda ans, a, x: unbroadcast_f( 144 | x, lambda g: -g * np.exp(-x + np.log(x) * (a - 1) - gammaln(a)) 145 | ), 146 | ) 147 | 148 | 149 | defvjp( 150 | gammaincinv, 151 | central_difference_of_(gammaincinv), 152 | lambda ans, a, y: unbroadcast_f( 153 | y, lambda g: g * np.exp(gammaincinv(a, y) - np.log(gammaincinv(a, y)) * (a - 1) + gammaln(a)) 154 | ), 155 | ) 156 | 157 | defvjp( 158 | gammainccinv, 159 | central_difference_of_(gammainccinv), 160 | lambda ans, a, y: unbroadcast_f( 161 | y, lambda g: -g * np.exp(gammainccinv(a, y) - np.log(gammainccinv(a, y)) * (a - 1) + gammaln(a)) 162 | ), 163 | ) 164 | 165 | defvjp( 166 | gammainccln, 167 | central_difference_of_log(gammaincc), 168 | lambda ans, a, x: unbroadcast_f( 169 | x, 170 | lambda g: -g 171 | * np.exp(-x + np.log(x) * (a - 1) - gammaln(a) - gammainccln(a, x)), 172 | ), 173 | ) 174 | 175 | defvjp( 176 | gammaincln, 177 | central_difference_of_log(gammainc), 178 | lambda ans, a, x: unbroadcast_f( 179 | x, 180 | lambda g: g * np.exp(-x + np.log(x) * (a - 1) - gammaln(a) - gammaincln(a, x)), 181 | ), 182 | ) 183 | 184 | 185 | defvjp( 186 | betainc, 187 | central_difference_of_(betainc, argnum=0), 188 | central_difference_of_(betainc, argnum=1), 189 | lambda ans, a, b, x: unbroadcast_f( 190 | x, lambda g: g * np.power(x, a - 1) * np.power(1 - x, b - 1) / beta(a, b) 191 | ), 192 | ) 193 | 194 | defvjp( 195 | betaincinv, 196 | central_difference_of_(betaincinv, argnum=0), 197 | central_difference_of_(betaincinv, argnum=1), 198 | lambda ans, a, b, y: unbroadcast_f( 199 | y, lambda g: g * 1/(np.power(betaincinv(a,b,y), a - 1) * np.power(1 - betaincinv(a,b,y), b - 1) / beta(a, b)) 200 | ), 201 | ) 202 | 203 | 204 | defvjp( 205 | betaincln, 206 | central_difference_of_log(betainc, argnum=0), 207 | central_difference_of_log(betainc, argnum=1), 208 | lambda ans, a, b, x: unbroadcast_f( 209 | x, 210 | lambda g: g 211 | * np.power(x, a - 1) 212 | * np.power(1 - x, b - 1) 213 | / beta(a, b) 214 | / betainc(a, b, x), 215 | ), 216 | ) 217 | --------------------------------------------------------------------------------