From edc54688d13ada88c6d0e1987f152fd7f2f3b91d Mon Sep 17 00:00:00 2001 From: Matthew Feickert Date: Tue, 30 Sep 2025 17:13:18 -0600 Subject: [PATCH 01/14] Remove tensorflow from pyproject.toml --- pyproject.toml | 30 ++---------------------------- 1 file changed, 2 insertions(+), 28 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3a18e9efd0..8ef6a9ebaf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,6 @@ keywords = [ "physics", "pytorch", "scipy", - "tensorflow", ] classifiers = [ "Development Status :: 4 - Beta", @@ -51,7 +50,7 @@ dependencies = [ "jsonschema>=4.15.0", # for utils "pyyaml>=5.1", # for parsing CLI equal-delimited options # c.f. https://github.com/scikit-hep/pyhf/issues/2593 for scipy v1.16.0 upper bound - "scipy>=1.5.2,<1.16.0", # requires numpy, which is required by pyhf and tensorflow + "scipy>=1.5.2,<1.16.0", # requires numpy, which is required by pyhf "tqdm>=4.56.0", # for readxml "numpy", # compatible versions controlled through scipy ] @@ -69,21 +68,6 @@ Homepage = "https://github.com/scikit-hep/pyhf" [project.optional-dependencies] shellcomplete = ["click_completion"] -# TODO: 'tensorflow' supports all platform_machine for tensorflow v2.16.1+ -# but TensorFlow only supports python_version 3.8 up through tensorflow v2.13.1. -# So until Python 3.8 support is dropped, split requirements on python_version -# before and after 3.9. -# NOTE: macos x86 support is deprecated from tensorflow v2.17.0 onwards. -tensorflow = [ - # python == 3.8 - "tensorflow>=2.7.0; python_version < '3.9' and platform_machine != 'arm64'", # c.f. PR #1962, #2452 - "tensorflow-macos>=2.7.0; python_version < '3.9' and platform_machine == 'arm64' and platform_system == 'Darwin'", # c.f. PR #2119, #2452 - "tensorflow-probability>=0.11.0; python_version < '3.9'", # c.f. PR #1657, #2452 - # python >= 3.9 - "tensorflow-probability[tf]>=0.24.0,<0.25.0; python_version >= '3.9' and platform_machine != 'arm64' and platform_system == 'Darwin'", # c.f. TensorFlow v2.17.0 - "tensorflow-probability[tf]>=0.24.0; python_version >= '3.9' and platform_machine == 'arm64' and platform_system == 'Darwin'", # c.f. TensorFlow v2.17.0 - "tensorflow-probability[tf]>=0.24.0; python_version >= '3.9' and platform_system != 'Darwin'" # c.f. TensorFlow v2.17.0 -] torch = [ "torch>=1.10.0", # c.f. PR #1657 "numpy<2.0" # c.f. https://github.com/pytorch/pytorch/issues/157973 @@ -98,7 +82,7 @@ contrib = [ "matplotlib>=3.0.0", "requests>=2.22.0", ] -backends = ["pyhf[tensorflow,torch,jax,minuit]"] +backends = ["pyhf[torch,jax,minuit]"] all = ["pyhf[backends,xmlio,contrib,shellcomplete]"] # Developer extras @@ -200,23 +184,19 @@ markers = [ "fail_numpy_minuit", "fail_pytorch", "fail_pytorch64", - "fail_tensorflow", "only_jax", "only_numpy", "only_numpy_minuit", "only_pytorch", "only_pytorch64", - "only_tensorflow", "skip_jax", "skip_numpy", "skip_numpy_minuit", "skip_pytorch", "skip_pytorch64", - "skip_tensorflow", ] filterwarnings = [ "error", - 'ignore:the imp module is deprecated:DeprecationWarning', # tensorflow 'ignore:distutils Version classes are deprecated:DeprecationWarning', # tensorflow-probability 'ignore:the `interpolation=` argument to percentile was renamed to `method=`, which has additional options:DeprecationWarning', # Issue #1772 "ignore:The interpolation= argument to 'quantile' is deprecated. Use 'method=' instead:DeprecationWarning", # Issue #1772 @@ -227,13 +207,9 @@ filterwarnings = [ 'ignore:Creating a tensor from a list of numpy.ndarrays is extremely slow. Please consider converting the list to a single numpy.ndarray with:UserWarning', #FIXME: tests/test_optim.py::test_minimize[no_grad-scipy-pytorch-no_stitch] 'ignore:divide by zero encountered in (true_)?divide:RuntimeWarning', #FIXME: pytest tests/test_tensor.py::test_pdf_calculations[numpy] 'ignore:[A-Z]+ is deprecated and will be removed in Pillow 10:DeprecationWarning', # keras - 'ignore:Call to deprecated create function:DeprecationWarning', # protobuf via tensorflow - 'ignore:`np.bool8` is a deprecated alias for `np.bool_`:DeprecationWarning', # numpy via tensorflow - "ignore:module 'sre_constants' is deprecated:DeprecationWarning", # tensorflow v2.12.0+ for Python 3.11+ "ignore:ml_dtypes.float8_e4m3b11 is deprecated.", #FIXME: Can remove when jaxlib>=0.4.12 "ignore:jsonschema.RefResolver is deprecated as of v4.18.0, in favor of the:DeprecationWarning", # Issue #2139 "ignore:Skipping device Apple Paravirtual device that does not support Metal 2.0:UserWarning", # Can't fix given hardware/virtualized device - 'ignore:Type google._upb._message.[A-Z]+ uses PyType_Spec with a metaclass that has custom:DeprecationWarning', # protobuf via tensorflow "ignore:jax.xla_computation is deprecated. Please use the AOT APIs:DeprecationWarning", # jax v0.4.30 "ignore:'MultiCommand' is deprecated and will be removed in Click 9.0. Use 'Group' instead.:DeprecationWarning", # Click "ignore:Jupyter is migrating its paths to use standard platformdirs:DeprecationWarning", # papermill @@ -268,7 +244,6 @@ module = [ 'jax.*', 'matplotlib.*', 'scipy.*', - 'tensorflow.*', 'tensorflow_probability.*', 'torch.*', 'uproot.*', @@ -300,7 +275,6 @@ module = [ 'pyhf.tensor.common.*', 'pyhf.tensor', 'pyhf.tensor.jax_backend.*', - 'pyhf.tensor.tensorflow_backend.*', 'pyhf.tensor.pytorch_backend.*', ] ignore_errors = true From 47e3efaf1878eedfc361fb33f42800a45cc947ed Mon Sep 17 00:00:00 2001 From: Matthew Feickert Date: Tue, 30 Sep 2025 17:16:07 -0600 Subject: [PATCH 02/14] Remove tesnorflow-probability from pyproject.toml --- pyproject.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8ef6a9ebaf..2e05c35c08 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -197,7 +197,6 @@ markers = [ ] filterwarnings = [ "error", - 'ignore:distutils Version classes are deprecated:DeprecationWarning', # tensorflow-probability 'ignore:the `interpolation=` argument to percentile was renamed to `method=`, which has additional options:DeprecationWarning', # Issue #1772 "ignore:The interpolation= argument to 'quantile' is deprecated. Use 'method=' instead:DeprecationWarning", # Issue #1772 'ignore: Exception ignored in:pytest.PytestUnraisableExceptionWarning', #FIXME: Exception ignored in: <_io.FileIO [closed]> @@ -244,7 +243,6 @@ module = [ 'jax.*', 'matplotlib.*', 'scipy.*', - 'tensorflow_probability.*', 'torch.*', 'uproot.*', ] From 7708c2a67f5c2c259e3c847d645da5f0460a117e Mon Sep 17 00:00:00 2001 From: Matthew Feickert Date: Tue, 30 Sep 2025 17:18:12 -0600 Subject: [PATCH 03/14] Drop tensorflow from citation info --- .zenodo.json | 1 - CITATION.cff | 1 - 2 files changed, 2 deletions(-) diff --git a/.zenodo.json b/.zenodo.json index 4d85eb20fb..99af30bbe6 100644 --- a/.zenodo.json +++ b/.zenodo.json @@ -28,7 +28,6 @@ "fitting", "scipy", "numpy", - "tensorflow", "pytorch", "jax", "auto-differentiation" diff --git a/CITATION.cff b/CITATION.cff index 14b1444bb7..a9262484db 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -26,7 +26,6 @@ keywords: - fitting - scipy - numpy - - tensorflow - pytorch - jax - auto-differentiation From b63103fdd5d64f40908126afe5bb24b382a03a43 Mon Sep 17 00:00:00 2001 From: Matthew Feickert Date: Tue, 30 Sep 2025 17:22:45 -0600 Subject: [PATCH 04/14] Remove tensorflow from docs --- docs/api.rst | 1 - docs/conf.py | 4 ---- docs/installation.rst | 14 -------------- 3 files changed, 19 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 56f65a211a..a7db15f1e7 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -66,7 +66,6 @@ The computational backends that :code:`pyhf` provides interfacing for the vector numpy_backend.numpy_backend pytorch_backend.pytorch_backend - tensorflow_backend.tensorflow_backend jax_backend.jax_backend Optimizers diff --git a/docs/conf.py b/docs/conf.py index dde69bd167..ca52d89898 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -146,11 +146,9 @@ def setup(app): # today_fmt = '%B %d, %Y' autodoc_mock_imports = [ - 'tensorflow', 'torch', 'jax', 'iminuit', - 'tensorflow_probability', ] @@ -195,7 +193,6 @@ def setup(app): 'examples/notebooks/ImpactPlot.ipynb', 'examples/notebooks/Recast.ipynb', 'examples/notebooks/StatError.ipynb', - 'examples/notebooks/example-tensorflow.ipynb', 'examples/notebooks/histogrammar.ipynb', 'examples/notebooks/histosys.ipynb', 'examples/notebooks/histosys-pytorch.ipynb', @@ -205,7 +202,6 @@ def setup(app): 'examples/notebooks/normsys.ipynb', 'examples/notebooks/pullplot.ipynb', 'examples/notebooks/pytorch_tests_onoff.ipynb', - 'examples/notebooks/tensorflow-limit.ipynb', ] # The reST default role (used for this markup: `text`) to use for all diff --git a/docs/installation.rst b/docs/installation.rst index 3cd820bc41..f856f2be07 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -27,13 +27,6 @@ Install latest stable release from `PyPI `__... python -m pip install pyhf -... with TensorFlow backend -+++++++++++++++++++++++++++ - -.. code-block:: console - - python -m pip install 'pyhf[tensorflow]' - ... with PyTorch backend ++++++++++++++++++++++++ @@ -74,13 +67,6 @@ Install latest development version from `GitHub Date: Tue, 30 Sep 2025 17:24:35 -0600 Subject: [PATCH 05/14] Remove tensorflow from gpu Docker --- docker/gpu/Dockerfile | 2 +- docker/gpu/install_backend.sh | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/docker/gpu/Dockerfile b/docker/gpu/Dockerfile index 288ef916f2..29f63ce090 100644 --- a/docker/gpu/Dockerfile +++ b/docker/gpu/Dockerfile @@ -13,7 +13,7 @@ RUN apt-get update -y && \ COPY . /code COPY ./docker/gpu/install_backend.sh /code/install_backend.sh WORKDIR /code -ARG BACKEND=tensorflow +ARG BACKEND=jax RUN python3 -m pip --no-cache-dir install --upgrade pip wheel && \ /bin/bash install_backend.sh ${BACKEND} && \ python3 -m pip list diff --git a/docker/gpu/install_backend.sh b/docker/gpu/install_backend.sh index 3185ff01cf..238f78acbe 100644 --- a/docker/gpu/install_backend.sh +++ b/docker/gpu/install_backend.sh @@ -18,10 +18,7 @@ function get_JAXLIB_GPU_WHEEL { function install_backend() { # 1: the backend option name in setup.py local backend="${1}" - if [[ "${backend}" == "tensorflow" ]]; then - # shellcheck disable=SC2102 - python3 -m pip install --no-cache-dir .[xmlio,tensorflow] - elif [[ "${backend}" == "torch" ]]; then + if [[ "${backend}" == "torch" ]]; then # shellcheck disable=SC2102 python3 -m pip install --no-cache-dir .[xmlio,torch] elif [[ "${backend}" == "jax" ]]; then From 361dc9181c95cd1ecfd1392148e179bd2e4d31cf Mon Sep 17 00:00:00 2001 From: Matthew Feickert Date: Tue, 30 Sep 2025 17:26:07 -0600 Subject: [PATCH 06/14] Remove tensorflow notebook --- .../notebooks/example-tensorflow.ipynb | 179 ------------------ 1 file changed, 179 deletions(-) delete mode 100644 docs/examples/notebooks/example-tensorflow.ipynb diff --git a/docs/examples/notebooks/example-tensorflow.ipynb b/docs/examples/notebooks/example-tensorflow.ipynb deleted file mode 100644 index 34863ebf73..0000000000 --- a/docs/examples/notebooks/example-tensorflow.ipynb +++ /dev/null @@ -1,179 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# TensorFlow" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Populating the interactive namespace from numpy and matplotlib\n" - ] - } - ], - "source": [ - "%pylab inline" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "import pyhf\n", - "from pyhf import Model\n", - "from pyhf.simplemodels import uncorrelated_background\n", - "import tensorflow as tf" - ] - }, - { - "cell_type": "code", - "execution_count": 45, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "---\n", - "as tensorflow\n", - "-----\n", - " [-22.877851486206055]\n" - ] - }, - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 45, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "source = {\n", - " \"binning\": [2, -0.5, 1.5],\n", - " \"bindata\": {\n", - " \"data\": [120.0, 180.0],\n", - " \"bkg\": [100.0, 150.0],\n", - " \"bkgerr\": [10.0, 10.0],\n", - " \"sig\": [30.0, 95.0],\n", - " },\n", - "}\n", - "\n", - "pdf = uncorrelated_background(\n", - " source['bindata']['sig'], source['bindata']['bkg'], source['bindata']['bkgerr']\n", - ")\n", - "data = source['bindata']['data'] + pdf.config.auxdata\n", - "\n", - "init_pars = pdf.config.suggested_init()\n", - "par_bounds = pdf.config.suggested_bounds()\n", - "\n", - "\n", - "print('---\\nas tensorflow\\n-----')\n", - "import tensorflow as tf\n", - "\n", - "pyhf.tensorlib = pyhf.tensorflow_backend()\n", - "v = pdf.logpdf(init_pars, data)\n", - "\n", - "pyhf.tensorlib.session = tf.Session()\n", - "print(type(v), pyhf.tensorlib.tolist(v))\n", - "\n", - "\n", - "from pathlib import Path\n", - "\n", - "tf.summary.FileWriter(Path.cwd(), pyhf.tensorlib.session.graph)" - ] - }, - { - "cell_type": "code", - "execution_count": 96, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[array([[4.]], dtype=float32), array([[12.]], dtype=float32)]" - ] - }, - "execution_count": 96, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "x = tf.Variable([1.0])\n", - "y = tf.Variable([2.0])\n", - "\n", - "z = x**2 * y + y**3 * x\n", - "\n", - "hessian = tf.hessians(z, [x, y])\n", - "\n", - "sess = tf.Session()\n", - "sess.run(tf.global_variables_initializer())\n", - "sess.run(hessian)" - ] - }, - { - "cell_type": "code", - "execution_count": 99, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[array([[4.]]), array([[12.]])]" - ] - }, - "execution_count": 99, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "x = tf.cast([1.0], tf.float64)\n", - "y = tf.cast([2.0], tf.float64)\n", - "\n", - "z = x**2 * y + y**3 * x\n", - "\n", - "hessian = tf.hessians(z, [x, y])\n", - "\n", - "sess = tf.Session()\n", - "sess.run(hessian)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.5" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} From 13c66ce88160f6d01516719026cfe2cafe50598c Mon Sep 17 00:00:00 2001 From: Matthew Feickert Date: Tue, 30 Sep 2025 17:28:16 -0600 Subject: [PATCH 07/14] Remove Edward notebook as it requires tensorflow --- docs/examples/experiments/edwardpyhf.ipynb | 126 --------------------- 1 file changed, 126 deletions(-) delete mode 100644 docs/examples/experiments/edwardpyhf.ipynb diff --git a/docs/examples/experiments/edwardpyhf.ipynb b/docs/examples/experiments/edwardpyhf.ipynb deleted file mode 100644 index e15afcb5c1..0000000000 --- a/docs/examples/experiments/edwardpyhf.ipynb +++ /dev/null @@ -1,126 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 67, - "metadata": {}, - "outputs": [], - "source": [ - "import tensorflow as tf\n", - "from edward.models import Poisson, Normal" - ] - }, - { - "cell_type": "code", - "execution_count": 78, - "metadata": {}, - "outputs": [], - "source": [ - "nuispar = tf.constant([3.0])\n", - "\n", - "x = Poisson(rate=tf.ones(1) * nuispar)\n", - "n = Normal(loc=nuispar, scale=tf.ones(1))\n", - "joined = tf.concat(\n", - " [x, n], axis=0\n", - ") # p(n, x | nuispar) = Pois(n|nuispar) * Normal(x |mu = nuispar, sigma = 1)" - ] - }, - { - "cell_type": "code", - "execution_count": 79, - "metadata": {}, - "outputs": [], - "source": [ - "results = []\n", - "for i in range(1000): # thee is probably a batched evaluation version .. this is stupid\n", - " with tf.Session() as sess:\n", - " r = sess.run(joined)\n", - " results.append(r)" - ] - }, - { - "cell_type": "code", - "execution_count": 80, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Populating the interactive namespace from numpy and matplotlib\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/lukas/.local/share/virtualenvs/pyhf-EFAVEj2h/lib/python3.6/site-packages/IPython/core/magics/pylab.py:160: UserWarning: pylab import has clobbered these variables: ['f']\n", - "`%matplotlib` prevents importing * from pylab and numpy\n", - " \"\\n`%matplotlib` prevents importing * from pylab and numpy\"\n" - ] - }, - { - "data": { - "text/plain": [ - "(array([ 6., 32., 62., 140., 188., 243., 191., 87., 41., 10.]),\n", - " array([-0.08035064, 0.51699646, 1.11434355, 1.71169064, 2.30903773,\n", - " 2.90638483, 3.50373192, 4.10107901, 4.6984261 , 5.2957732 ,\n", - " 5.89312029]),\n", - " )" - ] - }, - "execution_count": 80, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "%pylab inline\n", - "r = np.array(results)\n", - "f, axarr = plt.subplots(1, 3)\n", - "f.set_size_inches(12, 4)\n", - "axarr[0].scatter(r[:, 0], r[:, 1])\n", - "axarr[1].hist(r[:, 0], bins=np.linspace(0, 10, 11))\n", - "axarr[2].hist(r[:, 1])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.4" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} From 6ce0372a29112332fb360b69b6c8a6e09edb6412 Mon Sep 17 00:00:00 2001 From: Matthew Feickert Date: Tue, 30 Sep 2025 17:28:52 -0600 Subject: [PATCH 08/14] Remove tesnorflow limit notebook --- .../examples/notebooks/tensorflow-limit.ipynb | 166 ------------------ 1 file changed, 166 deletions(-) delete mode 100644 docs/examples/notebooks/tensorflow-limit.ipynb diff --git a/docs/examples/notebooks/tensorflow-limit.ipynb b/docs/examples/notebooks/tensorflow-limit.ipynb deleted file mode 100644 index 3b08cdf42d..0000000000 --- a/docs/examples/notebooks/tensorflow-limit.ipynb +++ /dev/null @@ -1,166 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# TensorFlow Limit" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import pyhf\n", - "import json\n", - "import logging\n", - "import numpy as np\n", - "from pyhf import runOnePoint, Model\n", - "from pyhf.simplemodels import uncorrelated_background\n", - "\n", - "\n", - "def invert_interval(testmus, cls_obs, cls_exp, test_size=0.05):\n", - " point05cross = {'exp': [], 'obs': None}\n", - " for cls_exp_sigma in cls_exp:\n", - " yvals = [x for x in cls_exp_sigma]\n", - " point05cross['exp'].append(\n", - " np.interp(test_size, list(reversed(yvals)), list(reversed(testmus)))\n", - " )\n", - "\n", - " yvals = cls_obs\n", - " point05cross['obs'] = np.interp(\n", - " test_size, list(reversed(yvals)), list(reversed(testmus))\n", - " )\n", - " return point05cross\n", - "\n", - "\n", - "def plot_results(testmus, cls_obs, cls_exp, test_size=0.05):\n", - " plt.plot(mutests, cls_obs, c='k')\n", - " for i, c in zip(range(5), ['grey', 'grey', 'grey', 'grey', 'grey']):\n", - " plt.plot(mutests, cls_exp[i], c=c)\n", - " plt.plot(testmus, [test_size] * len(testmus), c='r')\n", - " plt.ylim(0, 1)" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "import tensorflow as tf\n", - "\n", - "pyhf.tensorlib = pyhf.tensorflow_backend()\n", - "pyhf.tensorlib.session = tf.Session()\n", - "pyhf.optimizer = pyhf.tflow_optimizer(tensorlib=pyhf.tensorlib)" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Populating the interactive namespace from numpy and matplotlib\n" - ] - } - ], - "source": [ - "%pylab inline" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "mutest 0.0\n", - "mutest 0.5\n", - "mutest 1.0\n", - "mutest 1.5\n", - "mutest 2.0\n", - "mutest 2.5\n", - "mutest 3.0\n", - "mutest 3.5\n", - "mutest 4.0\n", - "mutest 4.5\n", - "mutest 5.0\n" - ] - }, - { - "data": { - "text/plain": [ - "{'exp': [1.1150276131816024,\n", - " 1.4635556988173117,\n", - " 2.0273926814698786,\n", - " 2.900217861845774,\n", - " 3.971687240957603],\n", - " 'obs': 2.42005196648799}" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "pdf = uncorrelated_background([10.0], [50.0], [7.0])\n", - "data = [55.0] + pdf.config.auxdata\n", - "\n", - "init_pars = pdf.config.suggested_init()\n", - "par_bounds = pdf.config.suggested_bounds()\n", - "\n", - "mutests = np.linspace(0, 5, 11)\n", - "tests = [\n", - " runOnePoint(muTest, data, pdf, init_pars, par_bounds)[-2:] for muTest in mutests\n", - "]\n", - "cls_obs = [test[0] for test in tests]\n", - "cls_exp = [[test[1][i] for test in tests] for i in range(5)]\n", - "\n", - "plot_results(mutests, cls_obs, cls_exp)\n", - "\n", - "invert_interval(mutests, cls_obs, cls_exp)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.4" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} From cfd8f55ad3a95084a7bbe1c9de77291b3bf15479 Mon Sep 17 00:00:00 2001 From: Matthew Feickert Date: Tue, 30 Sep 2025 17:30:33 -0600 Subject: [PATCH 09/14] Remove tensorflow from example histosys notebook --- docs/examples/notebooks/histosys-pytorch.ipynb | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/docs/examples/notebooks/histosys-pytorch.ipynb b/docs/examples/notebooks/histosys-pytorch.ipynb index 33a0d50ad8..10a83d641a 100644 --- a/docs/examples/notebooks/histosys-pytorch.ipynb +++ b/docs/examples/notebooks/histosys-pytorch.ipynb @@ -26,15 +26,13 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pyhf\n", "from pyhf import Model\n", - "from pyhf.simplemodels import uncorrelated_background\n", - "\n", - "import tensorflow as tf" + "from pyhf.simplemodels import uncorrelated_background" ] }, { @@ -78,7 +76,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -89,9 +87,6 @@ "# NumPy\n", " [-23.57960517]\n", "\n", - "# TensorFlow\n", - " Tensor(\"Reshape_1:0\", shape=(1,), dtype=float32)\n", - "\n", "# PyTorch\n", " tensor([-23.5796])\n" ] @@ -100,12 +95,10 @@ "source": [ "backends = [\n", " pyhf.tensor.numpy_backend(),\n", - " pyhf.tensor.tensorflow_backend(session=tf.Session()),\n", " pyhf.tensor.pytorch_backend(),\n", "]\n", "names = [\n", " 'NumPy',\n", - " 'TensorFlow',\n", " 'PyTorch',\n", "]\n", "\n", From ec42744bf0bf5e04e67155343aaa86523c5032ac Mon Sep 17 00:00:00 2001 From: Matthew Feickert Date: Tue, 30 Sep 2025 17:31:52 -0600 Subject: [PATCH 10/14] Remove tensorflow from source code Remove tensorflow backend Remove tensorflow optimizer shim Remove tensorflow opt_tflow remove tensorflow from source --- src/pyhf/optimize/common.py | 5 - src/pyhf/optimize/opt_tflow.py | 44 -- src/pyhf/tensor/__init__.py | 16 - src/pyhf/tensor/manager.py | 7 +- src/pyhf/tensor/tensorflow_backend.py | 725 -------------------------- 5 files changed, 1 insertion(+), 796 deletions(-) delete mode 100644 src/pyhf/optimize/opt_tflow.py delete mode 100644 src/pyhf/tensor/tensorflow_backend.py diff --git a/src/pyhf/optimize/common.py b/src/pyhf/optimize/common.py index 2049939159..1f95805d6c 100644 --- a/src/pyhf/optimize/common.py +++ b/src/pyhf/optimize/common.py @@ -42,11 +42,6 @@ def _get_tensor_shim(): return numpy_shim - if tensorlib.name == 'tensorflow': - from pyhf.optimize.opt_tflow import wrap_objective as tflow_shim - - return tflow_shim - if tensorlib.name == 'pytorch': from pyhf.optimize.opt_pytorch import wrap_objective as pytorch_shim diff --git a/src/pyhf/optimize/opt_tflow.py b/src/pyhf/optimize/opt_tflow.py deleted file mode 100644 index 178bc332ac..0000000000 --- a/src/pyhf/optimize/opt_tflow.py +++ /dev/null @@ -1,44 +0,0 @@ -"""Tensorflow Backend Function Shim.""" - -from pyhf import get_backend -import tensorflow as tf - - -def wrap_objective(objective, data, pdf, stitch_pars, do_grad=False, jit_pieces=None): - """ - Wrap the objective function for the minimization. - - Args: - objective (:obj:`func`): objective function - data (:obj:`list`): observed data - pdf (~pyhf.pdf.Model): The statistical model adhering to the schema model.json - stitch_pars (:obj:`func`): callable that stitches parameters, see :func:`pyhf.optimize.common.shim`. - do_grad (:obj:`bool`): enable autodifferentiation mode. Default is off. - - Returns: - objective_and_grad (:obj:`func`): tensor backend wrapped objective,gradient pair - """ - tensorlib, _ = get_backend() - - if do_grad: - - def func(pars): - pars = tensorlib.astensor(pars) - with tf.GradientTape() as tape: - tape.watch(pars) - constrained_pars = stitch_pars(pars) - constr_nll = objective(constrained_pars, data, pdf) - # NB: tape.gradient can return a sparse gradient (tf.IndexedSlices) - # when tf.gather is used and this needs to be converted back to a - # tensor to be usable as a value - grad = tape.gradient(constr_nll, pars) - return constr_nll.numpy()[0], tf.convert_to_tensor(grad) - - else: - - def func(pars): - pars = tensorlib.astensor(pars) - constrained_pars = stitch_pars(pars) - return objective(constrained_pars, data, pdf)[0] - - return func diff --git a/src/pyhf/tensor/__init__.py b/src/pyhf/tensor/__init__.py index a1036cf48e..832e7ce0f8 100644 --- a/src/pyhf/tensor/__init__.py +++ b/src/pyhf/tensor/__init__.py @@ -8,7 +8,6 @@ class _BackendRetriever: "jax_backend", "numpy_backend", "pytorch_backend", - "tensorflow_backend", ] def __init__(self): @@ -55,21 +54,6 @@ def __getattr__(self, name): "There was a problem importing PyTorch. The pytorch backend cannot be used.", e, ) - elif name == 'tensorflow_backend': - try: - from pyhf.tensor.tensorflow_backend import tensorflow_backend - - assert tensorflow_backend - # for autocomplete and dir() calls - self.tensorflow_backend = tensorflow_backend - self._array_types.add(tensorflow_backend.array_type) - self._array_subtypes.add(tensorflow_backend.array_subtype) - return tensorflow_backend - except ImportError as e: - raise exceptions.ImportBackendError( - "There was a problem importing TensorFlow. The tensorflow backend cannot be used.", - e, - ) @property def array_types(self): diff --git a/src/pyhf/tensor/manager.py b/src/pyhf/tensor/manager.py index c29a9bd45b..f56c8fa2ab 100644 --- a/src/pyhf/tensor/manager.py +++ b/src/pyhf/tensor/manager.py @@ -65,11 +65,6 @@ def set_backend( Example: >>> import pyhf - >>> pyhf.set_backend("tensorflow") - >>> pyhf.tensorlib.name - 'tensorflow' - >>> pyhf.tensorlib.precision - '64b' >>> pyhf.set_backend(b"pytorch", precision="32b") >>> pyhf.tensorlib.name 'pytorch' @@ -117,7 +112,7 @@ def set_backend( )(**backend_kwargs) except TypeError: raise exceptions.InvalidBackend( - f"The backend provided is not supported: {backend:s}. Select from one of the supported backends: numpy, tensorflow, pytorch" + f"The backend provided is not supported: {backend:s}. Select from one of the supported backends: numpy, pytorch" ) else: new_backend = backend diff --git a/src/pyhf/tensor/tensorflow_backend.py b/src/pyhf/tensor/tensorflow_backend.py deleted file mode 100644 index f15cf4cdac..0000000000 --- a/src/pyhf/tensor/tensorflow_backend.py +++ /dev/null @@ -1,725 +0,0 @@ -"""Tensorflow Tensor Library Module.""" - -import logging -import tensorflow as tf -import tensorflow_probability as tfp - -log = logging.getLogger(__name__) - - -class tensorflow_backend: - """TensorFlow backend for pyhf""" - - __slots__ = ['default_do_grad', 'dtypemap', 'name', 'precision'] - - #: The array type for tensorflow - array_type = tf.Tensor - - #: The array content type for tensorflow - array_subtype = tf.Tensor - - def __init__(self, **kwargs): - self.name = 'tensorflow' - self.precision = kwargs.get('precision', '64b') - self.dtypemap = { - 'float': tf.float64 if self.precision == '64b' else tf.float32, - 'int': tf.int64 if self.precision == '64b' else tf.int32, - 'bool': tf.bool, - } - self.default_do_grad = True - - def _setup(self): - """ - Run any global setups for the tensorflow lib. - """ - - def clip(self, tensor_in, min_value, max_value): - """ - Clips (limits) the tensor values to be within a specified min and max. - - Example: - >>> import pyhf - >>> pyhf.set_backend("tensorflow") - >>> a = pyhf.tensorlib.astensor([-2, -1, 0, 1, 2]) - >>> t = pyhf.tensorlib.clip(a, -1, 1) - >>> print(t) - tf.Tensor([-1. -1. 0. 1. 1.], shape=(5,), dtype=float64) - - Args: - tensor_in (:obj:`tensor`): The input tensor object - min_value (:obj:`scalar` or :obj:`tensor` or :obj:`None`): The minimum value to be clipped to - max_value (:obj:`scalar` or :obj:`tensor` or :obj:`None`): The maximum value to be clipped to - - Returns: - TensorFlow Tensor: A clipped `tensor` - - """ - if min_value is None: - min_value = tf.reduce_min(tensor_in) - if max_value is None: - max_value = tf.reduce_max(tensor_in) - return tf.clip_by_value(tensor_in, min_value, max_value) - - def erf(self, tensor_in): - """ - The error function of complex argument. - - Example: - - >>> import pyhf - >>> pyhf.set_backend("tensorflow") - >>> a = pyhf.tensorlib.astensor([-2., -1., 0., 1., 2.]) - >>> t = pyhf.tensorlib.erf(a) - >>> print(t) - tf.Tensor([-0.99532227 -0.84270079 0. 0.84270079 0.99532227], shape=(5,), dtype=float64) - - Args: - tensor_in (:obj:`tensor`): The input tensor object - - Returns: - TensorFlow Tensor: The values of the error function at the given points. - """ - return tf.math.erf(tensor_in) - - def erfinv(self, tensor_in): - """ - The inverse of the error function of complex argument. - - Example: - - >>> import pyhf - >>> pyhf.set_backend("tensorflow") - >>> a = pyhf.tensorlib.astensor([-2., -1., 0., 1., 2.]) - >>> t = pyhf.tensorlib.erfinv(pyhf.tensorlib.erf(a)) - >>> print(t) - tf.Tensor([-2. -1. 0. 1. 2.], shape=(5,), dtype=float64) - - Args: - tensor_in (:obj:`tensor`): The input tensor object - - Returns: - TensorFlow Tensor: The values of the inverse of the error function at the given points. - """ - return tf.math.erfinv(tensor_in) - - def tile(self, tensor_in, repeats): - """ - Repeat tensor data along a specific dimension - - Example: - >>> import pyhf - >>> pyhf.set_backend("tensorflow") - >>> a = pyhf.tensorlib.astensor([[1.0], [2.0]]) - >>> t = pyhf.tensorlib.tile(a, (1, 2)) - >>> print(t) - tf.Tensor( - [[1. 1.] - [2. 2.]], shape=(2, 2), dtype=float64) - - Args: - tensor_in (:obj:`tensor`): The tensor to be repeated - repeats (:obj:`tensor`): The tuple of multipliers for each dimension - - Returns: - TensorFlow Tensor: The tensor with repeated axes - - """ - try: - return tf.tile(tensor_in, repeats) - except tf.errors.InvalidArgumentError: - shape = tf.shape(tensor_in).numpy().tolist() - diff = len(repeats) - len(shape) - if diff < 0: - raise - return tf.tile(tf.reshape(tensor_in, [1] * diff + shape), repeats) - - def conditional(self, predicate, true_callable, false_callable): - """ - Runs a callable conditional on the boolean value of the evaluation of a predicate - - Example: - >>> import pyhf - >>> pyhf.set_backend("tensorflow") - >>> tensorlib = pyhf.tensorlib - >>> a = tensorlib.astensor([4]) - >>> b = tensorlib.astensor([5]) - >>> t = tensorlib.conditional((a < b)[0], lambda: a + b, lambda: a - b) - >>> print(t) - tf.Tensor([9.], shape=(1,), dtype=float64) - - Args: - predicate (:obj:`scalar`): The logical condition that determines which callable to evaluate - true_callable (:obj:`callable`): The callable that is evaluated when the :code:`predicate` evaluates to :code:`true` - false_callable (:obj:`callable`): The callable that is evaluated when the :code:`predicate` evaluates to :code:`false` - - Returns: - TensorFlow Tensor: The output of the callable that was evaluated - - """ - return tf.cond(predicate, true_callable, false_callable) - - def tolist(self, tensor_in): - try: - return tensor_in.numpy().tolist() - except AttributeError: - if isinstance(tensor_in, list): - return tensor_in - raise - - def outer(self, tensor_in_1, tensor_in_2): - dtype = self.dtypemap["float"] - tensor_in_1 = ( - tensor_in_1 if tensor_in_1.dtype != tf.bool else tf.cast(tensor_in_1, dtype) - ) - tensor_in_1 = ( - tensor_in_1 if tensor_in_2.dtype != tf.bool else tf.cast(tensor_in_2, dtype) - ) - return tf.einsum('i,j->ij', tensor_in_1, tensor_in_2) - - def gather(self, tensor, indices): - return tf.compat.v2.gather(tensor, indices) - - def boolean_mask(self, tensor, mask): - return tf.boolean_mask(tensor, mask) - - def isfinite(self, tensor): - return tf.math.is_finite(tensor) - - def astensor(self, tensor_in, dtype='float'): - """ - Convert to a TensorFlow Tensor. - - Example: - - >>> import pyhf - >>> pyhf.set_backend("tensorflow") - >>> tensor = pyhf.tensorlib.astensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]) - >>> tensor - - >>> type(tensor) - - - Args: - tensor_in (Number or Tensor): Tensor object - - Returns: - `tf.Tensor`: A symbolic handle to one of the outputs of a `tf.Operation`. - - """ - try: - dtype = self.dtypemap[dtype] - except KeyError: - log.error( - 'Invalid dtype: dtype must be float, int, or bool.', exc_info=True - ) - raise - - tensor = tensor_in - # If already a tensor then done - try: - # Use a tensor attribute that isn't meaningless when eager execution is enabled - tensor.device - except AttributeError: - tensor = tf.convert_to_tensor(tensor_in) - if tensor.dtype is not dtype: - tensor = tf.cast(tensor, dtype) - return tensor - - def sum(self, tensor_in, axis=None): - return ( - tf.reduce_sum(tensor_in) - if (axis is None or tensor_in.shape == tf.TensorShape([])) - else tf.reduce_sum(tensor_in, axis) - ) - - def product(self, tensor_in, axis=None): - return ( - tf.reduce_prod(tensor_in) - if axis is None - else tf.reduce_prod(tensor_in, axis) - ) - - def abs(self, tensor): - return tf.abs(tensor) - - def ones(self, shape, dtype="float"): - try: - dtype = self.dtypemap[dtype] - except KeyError: - log.error( - f"Invalid dtype: dtype must be one of {list(self.dtypemap)}.", - exc_info=True, - ) - raise - - return tf.ones(shape, dtype=dtype) - - def zeros(self, shape, dtype="float"): - try: - dtype = self.dtypemap[dtype] - except KeyError: - log.error( - f"Invalid dtype: dtype must be one of {list(self.dtypemap)}.", - exc_info=True, - ) - raise - - return tf.zeros(shape, dtype=dtype) - - def power(self, tensor_in_1, tensor_in_2): - return tf.pow(tensor_in_1, tensor_in_2) - - def sqrt(self, tensor_in): - return tf.sqrt(tensor_in) - - def shape(self, tensor): - return tuple(map(int, tensor.shape)) - - def reshape(self, tensor, newshape): - return tf.reshape(tensor, newshape) - - def ravel(self, tensor): - """ - Return a flattened view of the tensor, not a copy. - - Example: - - >>> import pyhf - >>> pyhf.set_backend("tensorflow") - >>> tensor = pyhf.tensorlib.astensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]) - >>> t_ravel = pyhf.tensorlib.ravel(tensor) - >>> print(t_ravel) - tf.Tensor([1. 2. 3. 4. 5. 6.], shape=(6,), dtype=float64) - - Args: - tensor (Tensor): Tensor object - - Returns: - `tf.Tensor`: A flattened array. - """ - return self.reshape(tensor, -1) - - def divide(self, tensor_in_1, tensor_in_2): - return tf.divide(tensor_in_1, tensor_in_2) - - def log(self, tensor_in): - return tf.math.log(tensor_in) - - def exp(self, tensor_in): - return tf.exp(tensor_in) - - def percentile(self, tensor_in, q, axis=None, interpolation="linear"): - r""" - Compute the :math:`q`-th percentile of the tensor along the specified axis. - - Example: - - >>> import pyhf - >>> pyhf.set_backend("tensorflow") - >>> a = pyhf.tensorlib.astensor([[10, 7, 4], [3, 2, 1]]) - >>> t = pyhf.tensorlib.percentile(a, 50) - >>> print(t) - tf.Tensor(3.5, shape=(), dtype=float64) - >>> t = pyhf.tensorlib.percentile(a, 50, axis=1) - >>> print(t) - tf.Tensor([7. 2.], shape=(2,), dtype=float64) - - Args: - tensor_in (`tensor`): The tensor containing the data - q (:obj:`float` or `tensor`): The :math:`q`-th percentile to compute - axis (`number` or `tensor`): The dimensions along which to compute - interpolation (:obj:`str`): The interpolation method to use when the - desired percentile lies between two data points ``i < j``: - - - ``'linear'``: ``i + (j - i) * fraction``, where ``fraction`` is the - fractional part of the index surrounded by ``i`` and ``j``. - - - ``'lower'``: ``i``. - - - ``'higher'``: ``j``. - - - ``'midpoint'``: ``(i + j) / 2``. - - - ``'nearest'``: ``i`` or ``j``, whichever is nearest. - - Returns: - TensorFlow Tensor: The value of the :math:`q`-th percentile of the tensor along the specified axis. - - .. versionadded:: 0.7.0 - """ - return tfp.stats.percentile( - tensor_in, q, axis=axis, interpolation=interpolation - ) - - def stack(self, sequence, axis=0): - return tf.stack(sequence, axis=axis) - - def where(self, mask, tensor_in_1, tensor_in_2): - """ - Apply a boolean selection mask to the elements of the input tensors. - - Example: - - >>> import pyhf - >>> pyhf.set_backend("tensorflow") - >>> t = pyhf.tensorlib.where( - ... pyhf.tensorlib.astensor([1, 0, 1], dtype='bool'), - ... pyhf.tensorlib.astensor([1, 1, 1]), - ... pyhf.tensorlib.astensor([2, 2, 2]), - ... ) - >>> print(t) - tf.Tensor([1. 2. 1.], shape=(3,), dtype=float64) - - Args: - mask (bool): Boolean mask (boolean or tensor object of booleans) - tensor_in_1 (Tensor): Tensor object - tensor_in_2 (Tensor): Tensor object - - Returns: - TensorFlow Tensor: The result of the mask being applied to the tensors. - - """ - return tf.where(mask, tensor_in_1, tensor_in_2) - - def concatenate(self, sequence, axis=0): - """ - Join a sequence of arrays along an existing axis. - - Args: - sequence: sequence of tensors - axis: dimension along which to concatenate - - Returns: - output: the concatenated tensor - - """ - return tf.concat(sequence, axis=axis) - - def simple_broadcast(self, *args): - """ - Broadcast a sequence of 1 dimensional arrays. - - Example: - >>> import pyhf - >>> pyhf.set_backend("tensorflow") - >>> b = pyhf.tensorlib.simple_broadcast( - ... pyhf.tensorlib.astensor([1]), - ... pyhf.tensorlib.astensor([2, 3, 4]), - ... pyhf.tensorlib.astensor([5, 6, 7])) - >>> print([str(t) for t in b]) # doctest: +NORMALIZE_WHITESPACE - ['tf.Tensor([1. 1. 1.], shape=(3,), dtype=float64)', - 'tf.Tensor([2. 3. 4.], shape=(3,), dtype=float64)', - 'tf.Tensor([5. 6. 7.], shape=(3,), dtype=float64)'] - - Args: - args (Array of Tensors): Sequence of arrays - - Returns: - list of Tensors: The sequence broadcast together. - - """ - - max_dim = max(map(tf.size, args)) - try: - assert not [arg for arg in args if 1 < tf.size(arg) < max_dim] - except AssertionError: - log.error( - 'ERROR: The arguments must be of compatible size: 1 or %i', max_dim - ) - raise - return [tf.broadcast_to(arg, (max_dim,)) for arg in args] - - def einsum(self, subscripts, *operands): - """ - A generalized contraction between tensors of arbitrary dimension. - - This function returns a tensor whose elements are defined by equation, - which is written in a shorthand form inspired by the Einstein summation - convention. - - Args: - subscripts: str, specifies the subscripts for summation - operands: list of array_like, these are the tensors for the operation - - Returns: - TensorFlow Tensor: the calculation based on the Einstein summation convention - """ - return tf.einsum(subscripts, *operands) - - def poisson_logpdf(self, n, lam): - r""" - The log of the continuous approximation, using :math:`n! = \Gamma\left(n+1\right)`, - to the probability mass function of the Poisson distribution evaluated - at :code:`n` given the parameter :code:`lam`. - - Example: - >>> import pyhf - >>> pyhf.set_backend("tensorflow") - >>> t = pyhf.tensorlib.poisson_logpdf(5., 6.) - >>> print(t) # doctest:+ELLIPSIS - tf.Tensor(-1.82869439..., shape=(), dtype=float64) - >>> values = pyhf.tensorlib.astensor([5., 9.]) - >>> rates = pyhf.tensorlib.astensor([6., 8.]) - >>> t = pyhf.tensorlib.poisson_logpdf(values, rates) - >>> print(t) - tf.Tensor([-1.8286944 -2.0868536], shape=(2,), dtype=float64) - - Args: - n (:obj:`tensor` or :obj:`float`): The value at which to evaluate the approximation to the Poisson distribution p.m.f. - (the observed number of events) - lam (:obj:`tensor` or :obj:`float`): The mean of the Poisson distribution p.m.f. - (the expected number of events) - - Returns: - TensorFlow Tensor: Value of the continuous approximation to log(Poisson(n|lam)) - """ - lam = self.astensor(lam) - return tfp.distributions.Poisson(lam).log_prob(n) - - def poisson(self, n, lam): - r""" - The continuous approximation, using :math:`n! = \Gamma\left(n+1\right)`, - to the probability mass function of the Poisson distribution evaluated - at :code:`n` given the parameter :code:`lam`. - - .. note:: - - Though the p.m.f of the Poisson distribution is not defined for - :math:`\lambda = 0`, the limit as :math:`\lambda \to 0` is still - defined, which gives a degenerate p.m.f. of - - .. math:: - - \lim_{\lambda \to 0} \,\mathrm{Pois}(n | \lambda) = - \left\{\begin{array}{ll} - 1, & n = 0,\\ - 0, & n > 0 - \end{array}\right. - - Example: - >>> import pyhf - >>> pyhf.set_backend("tensorflow") - >>> t = pyhf.tensorlib.poisson(5., 6.) - >>> print(t) # doctest:+ELLIPSIS - tf.Tensor(0.16062314..., shape=(), dtype=float64) - >>> values = pyhf.tensorlib.astensor([5., 9.]) - >>> rates = pyhf.tensorlib.astensor([6., 8.]) - >>> t = pyhf.tensorlib.poisson(values, rates) - >>> print(t) - tf.Tensor([0.16062314 0.12407692], shape=(2,), dtype=float64) - - Args: - n (:obj:`tensor` or :obj:`float`): The value at which to evaluate the approximation to the Poisson distribution p.m.f. - (the observed number of events) - lam (:obj:`tensor` or :obj:`float`): The mean of the Poisson distribution p.m.f. - (the expected number of events) - - Returns: - TensorFlow Tensor: Value of the continuous approximation to Poisson(n|lam) - """ - lam = self.astensor(lam) - return tf.exp(tfp.distributions.Poisson(lam).log_prob(n)) - - def normal_logpdf(self, x, mu, sigma): - r""" - The log of the probability density function of the Normal distribution evaluated - at :code:`x` given parameters of mean of :code:`mu` and standard deviation - of :code:`sigma`. - - Example: - >>> import pyhf - >>> pyhf.set_backend("tensorflow") - >>> t = pyhf.tensorlib.normal_logpdf(0.5, 0., 1.) - >>> print(t) # doctest:+ELLIPSIS - tf.Tensor(-1.04393853..., shape=(), dtype=float64) - >>> values = pyhf.tensorlib.astensor([0.5, 2.0]) - >>> means = pyhf.tensorlib.astensor([0., 2.3]) - >>> sigmas = pyhf.tensorlib.astensor([1., 0.8]) - >>> t = pyhf.tensorlib.normal_logpdf(values, means, sigmas) - >>> print(t) - tf.Tensor([-1.04393853 -0.76610747], shape=(2,), dtype=float64) - - Args: - x (:obj:`tensor` or :obj:`float`): The value at which to evaluate the Normal distribution p.d.f. - mu (:obj:`tensor` or :obj:`float`): The mean of the Normal distribution - sigma (:obj:`tensor` or :obj:`float`): The standard deviation of the Normal distribution - - Returns: - TensorFlow Tensor: Value of log(Normal(x|mu, sigma)) - """ - mu = self.astensor(mu) - sigma = self.astensor(sigma) - - return tfp.distributions.Normal(mu, sigma).log_prob(x) - - def normal(self, x, mu, sigma): - r""" - The probability density function of the Normal distribution evaluated - at :code:`x` given parameters of mean of :code:`mu` and standard deviation - of :code:`sigma`. - - Example: - >>> import pyhf - >>> pyhf.set_backend("tensorflow") - >>> t = pyhf.tensorlib.normal(0.5, 0., 1.) - >>> print(t) # doctest:+ELLIPSIS - tf.Tensor(0.35206532..., shape=(), dtype=float64) - >>> values = pyhf.tensorlib.astensor([0.5, 2.0]) - >>> means = pyhf.tensorlib.astensor([0., 2.3]) - >>> sigmas = pyhf.tensorlib.astensor([1., 0.8]) - >>> t = pyhf.tensorlib.normal(values, means, sigmas) - >>> print(t) - tf.Tensor([0.35206533 0.46481887], shape=(2,), dtype=float64) - - Args: - x (:obj:`tensor` or :obj:`float`): The value at which to evaluate the Normal distribution p.d.f. - mu (:obj:`tensor` or :obj:`float`): The mean of the Normal distribution - sigma (:obj:`tensor` or :obj:`float`): The standard deviation of the Normal distribution - - Returns: - TensorFlow Tensor: Value of Normal(x|mu, sigma) - """ - mu = self.astensor(mu) - sigma = self.astensor(sigma) - - return tfp.distributions.Normal(mu, sigma).prob(x) - - def normal_cdf(self, x, mu=0.0, sigma=1): - """ - Compute the value of cumulative distribution function for the Normal distribution at x. - - Example: - >>> import pyhf - >>> pyhf.set_backend("tensorflow") - >>> t = pyhf.tensorlib.normal_cdf(0.8) - >>> print(t) # doctest:+ELLIPSIS - tf.Tensor(0.78814460..., shape=(), dtype=float64) - >>> values = pyhf.tensorlib.astensor([0.8, 2.0]) - >>> t = pyhf.tensorlib.normal_cdf(values) - >>> print(t) - tf.Tensor([0.7881446 0.97724987], shape=(2,), dtype=float64) - - Args: - x (:obj:`tensor` or :obj:`float`): The observed value of the random variable to evaluate the CDF for - mu (:obj:`tensor` or :obj:`float`): The mean of the Normal distribution - sigma (:obj:`tensor` or :obj:`float`): The standard deviation of the Normal distribution - - Returns: - TensorFlow Tensor: The CDF - """ - mu = self.astensor(mu) - sigma = self.astensor(sigma) - - return tfp.distributions.Normal(mu, sigma).cdf(x) - - def poisson_dist(self, rate): - r""" - Construct a Poisson distribution with rate parameter :code:`rate`. - - Example: - >>> import pyhf - >>> pyhf.set_backend("tensorflow") - >>> rates = pyhf.tensorlib.astensor([5, 8]) - >>> values = pyhf.tensorlib.astensor([4, 9]) - >>> poissons = pyhf.tensorlib.poisson_dist(rates) - >>> t = poissons.log_prob(values) - >>> print(t) - tf.Tensor([-1.74030218 -2.0868536 ], shape=(2,), dtype=float64) - - Args: - rate (:obj:`tensor` or :obj:`float`): The mean of the Poisson distribution (the expected number of events) - - Returns: - TensorFlow Probability Poisson distribution: The Poisson distribution class - - """ - rate = self.astensor(rate) - - return tfp.distributions.Poisson(rate) - - def normal_dist(self, mu, sigma): - r""" - Construct a Normal distribution with mean :code:`mu` and standard deviation :code:`sigma`. - - Example: - >>> import pyhf - >>> pyhf.set_backend("tensorflow") - >>> means = pyhf.tensorlib.astensor([5, 8]) - >>> stds = pyhf.tensorlib.astensor([1, 0.5]) - >>> values = pyhf.tensorlib.astensor([4, 9]) - >>> normals = pyhf.tensorlib.normal_dist(means, stds) - >>> t = normals.log_prob(values) - >>> print(t) - tf.Tensor([-1.41893853 -2.22579135], shape=(2,), dtype=float64) - - Args: - mu (:obj:`tensor` or :obj:`float`): The mean of the Normal distribution - sigma (:obj:`tensor` or :obj:`float`): The standard deviation of the Normal distribution - - Returns: - TensorFlow Probability Normal distribution: The Normal distribution class - - """ - mu = self.astensor(mu) - sigma = self.astensor(sigma) - - return tfp.distributions.Normal(mu, sigma) - - def to_numpy(self, tensor_in): - """ - Convert the TensorFlow tensor to a :class:`numpy.ndarray`. - - Example: - >>> import pyhf - >>> pyhf.set_backend("tensorflow") - >>> tensor = pyhf.tensorlib.astensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]) - >>> print(tensor) - tf.Tensor( - [[1. 2. 3.] - [4. 5. 6.]], shape=(2, 3), dtype=float64) - >>> numpy_ndarray = pyhf.tensorlib.to_numpy(tensor) - >>> numpy_ndarray - array([[1., 2., 3.], - [4., 5., 6.]]) - >>> type(numpy_ndarray) - - - Args: - tensor_in (:obj:`tensor`): The input tensor object. - - Returns: - :class:`numpy.ndarray`: The tensor converted to a NumPy ``ndarray``. - - """ - return tensor_in.numpy() - - def transpose(self, tensor_in): - """ - Transpose the tensor. - - Example: - >>> import pyhf - >>> pyhf.set_backend("tensorflow") - >>> tensor = pyhf.tensorlib.astensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]) - >>> print(tensor) - tf.Tensor( - [[1. 2. 3.] - [4. 5. 6.]], shape=(2, 3), dtype=float64) - >>> tensor_T = pyhf.tensorlib.transpose(tensor) - >>> print(tensor_T) - tf.Tensor( - [[1. 4.] - [2. 5.] - [3. 6.]], shape=(3, 2), dtype=float64) - - Args: - tensor_in (:obj:`tensor`): The input tensor object. - - Returns: - TensorFlow Tensor: The transpose of the input tensor. - - .. versionadded:: 0.7.0 - """ - return tf.transpose(tensor_in) From 05629bd5860cb8b92165719f74bcd3827641bd58 Mon Sep 17 00:00:00 2001 From: Matthew Feickert Date: Tue, 30 Sep 2025 17:34:15 -0600 Subject: [PATCH 11/14] Remove tensorflow from CLI --- src/pyhf/cli/infer.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/pyhf/cli/infer.py b/src/pyhf/cli/infer.py index f2b0dce107..2212cb1e78 100644 --- a/src/pyhf/cli/infer.py +++ b/src/pyhf/cli/infer.py @@ -36,7 +36,7 @@ def cli(): ) @click.option( "--backend", - type=click.Choice(["numpy", "pytorch", "tensorflow", "jax", "np", "torch", "tf"]), + type=click.Choice(["numpy", "pytorch", "jax", "np", "torch"]), help="The tensor backend used for the calculation.", default="numpy", ) @@ -83,8 +83,6 @@ def fit( # set the backend if not NumPy if backend in ["pytorch", "torch"]: set_backend("pytorch", precision="64b") - elif backend in ["tensorflow", "tf"]: - set_backend("tensorflow", precision="64b") elif backend in ["jax"]: set_backend("jax") tensorlib, _ = get_backend() @@ -150,7 +148,7 @@ def fit( ) @click.option( '--backend', - type=click.Choice(['numpy', 'pytorch', 'tensorflow', 'jax', 'np', 'torch', 'tf']), + type=click.Choice(['numpy', 'pytorch', 'jax', 'np', 'torch']), help='The tensor backend used for the calculation.', default='numpy', ) @@ -215,8 +213,6 @@ def cls( # set the backend if not NumPy if backend in ['pytorch', 'torch']: set_backend("pytorch", precision="64b") - elif backend in ['tensorflow', 'tf']: - set_backend("tensorflow", precision="64b") elif backend in ['jax']: set_backend("jax") tensorlib, _ = get_backend() From 002e402b2196a91db9cf15f2adb760fefd942d56 Mon Sep 17 00:00:00 2001 From: Matthew Feickert Date: Tue, 30 Sep 2025 17:38:49 -0600 Subject: [PATCH 12/14] Remove tensorflow from test constraints --- tests/constraints.txt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/constraints.txt b/tests/constraints.txt index 7954b2be44..4596667894 100644 --- a/tests/constraints.txt +++ b/tests/constraints.txt @@ -11,10 +11,6 @@ numpy==1.21.0 # constrained by jax v0.4.1 uproot==4.1.1 # minuit iminuit==2.7.0 # c.f. PR #1895 -# tensorflow -tensorflow==2.7.0 # c.f. PR #1962 -tensorflow-probability==0.11.0 # c.f. PR #1657 -protobuf<4.21.0 # c.f. PR #2117 # torch torch==1.10.0 # jax From ee50925f10d09b3b127b8996c1279a9104d7b421 Mon Sep 17 00:00:00 2001 From: Matthew Feickert Date: Tue, 30 Sep 2025 17:39:51 -0600 Subject: [PATCH 13/14] Remove tensorflow from tests --- tests/conftest.py | 7 +++---- tests/test_backend_consistency.py | 1 - tests/test_init.py | 8 +------- tests/test_interpolate.py | 2 +- tests/test_optim.py | 12 ++--------- tests/test_public_api.py | 8 ++++---- tests/test_scripts.py | 4 ++-- tests/test_simplemodels.py | 2 -- tests/test_tensor.py | 34 ++++++------------------------- tests/test_validation.py | 2 -- 10 files changed, 19 insertions(+), 61 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 7e2e9458f4..a0e19207eb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,7 +16,7 @@ def pytest_addoption(parser): action="append", type=str, default=[], - choices=["tensorflow", "pytorch", "jax", "minuit"], + choices=["pytorch", "jax", "minuit"], help="list of backends to disable in tests", ) parser.addoption( @@ -80,19 +80,18 @@ def reset_backend(): (("numpy_backend", dict()), ("scipy_optimizer", dict())), (("pytorch_backend", dict()), ("scipy_optimizer", dict())), (("pytorch_backend", dict(precision="64b")), ("scipy_optimizer", dict())), - (("tensorflow_backend", dict()), ("scipy_optimizer", dict())), (("jax_backend", dict()), ("scipy_optimizer", dict())), ( ("numpy_backend", dict(poisson_from_normal=True)), ("minuit_optimizer", dict()), ), ], - ids=['numpy', 'pytorch', 'pytorch64', 'tensorflow', 'jax', 'numpy_minuit'], + ids=['numpy', 'pytorch', 'pytorch64', 'jax', 'numpy_minuit'], ) def backend(request): # a better way to get the id? all the backends we have so far for testing param_ids = request._fixturedef.ids - # the backend we're using: numpy, tensorflow, etc... + # the backend we're using: numpy, etc... param_id = param_ids[request.param_index] # name of function being called (with params), the original name is .originalname func_name = request._pyfuncitem.name diff --git a/tests/test_backend_consistency.py b/tests/test_backend_consistency.py index 512b06b707..4d65799904 100644 --- a/tests/test_backend_consistency.py +++ b/tests/test_backend_consistency.py @@ -105,7 +105,6 @@ def test_hypotest_qmu_tilde( backends = [ pyhf.tensor.numpy_backend(precision='64b'), - pyhf.tensor.tensorflow_backend(precision='64b'), pyhf.tensor.pytorch_backend(precision='64b'), pyhf.tensor.jax_backend(precision='64b'), ] diff --git a/tests/test_init.py b/tests/test_init.py index f86925275c..c247d40ac9 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -13,12 +13,6 @@ "pytorch_backend", pytest.raises(pyhf.exceptions.ImportBackendError), ], - [ - "tensorflow", - "tensorflow_backend", - "tensorflow_backend", - pytest.raises(pyhf.exceptions.ImportBackendError), - ], [ "jax", "jax_backend", @@ -26,7 +20,7 @@ pytest.raises(pyhf.exceptions.ImportBackendError), ], ], - ids=["numpy", "pytorch", "tensorflow", "jax"], + ids=["numpy", "pytorch", "jax"], ) def test_missing_backends(isolate_modules, param): backend_name, module_name, import_name, expectation = param diff --git a/tests/test_interpolate.py b/tests/test_interpolate.py index 52b5830f12..cb5d605368 100644 --- a/tests/test_interpolate.py +++ b/tests/test_interpolate.py @@ -119,7 +119,7 @@ def test_validate_implementation(backend, interpcode, random_histosets_alphasets histogramssets, alphasets = random_histosets_alphasets_pair # single-float precision backends, calculate using single-floats - if pyhf.tensorlib.name in ['tensorflow', 'pytorch']: + if pyhf.tensorlib.name in ['pytorch']: abs_tolerance = 1e-6 histogramssets = np.asarray(histogramssets, dtype=np.float32) alphasets = np.asarray(alphasets, dtype=np.float32) diff --git a/tests/test_optim.py b/tests/test_optim.py index cfd7b0890a..fdb6707718 100644 --- a/tests/test_optim.py +++ b/tests/test_optim.py @@ -14,7 +14,6 @@ # from https://docs.scipy.org/doc/scipy/tutorial/optimize.html#nelder-mead-simplex-algorithm-method-nelder-mead @pytest.mark.skip_pytorch @pytest.mark.skip_pytorch64 -@pytest.mark.skip_tensorflow @pytest.mark.skip_numpy_minuit def test_scipy_minimize(backend, capsys): tensorlib, _ = backend @@ -36,10 +35,9 @@ def rosen(x): [ pyhf.tensor.numpy_backend, pyhf.tensor.pytorch_backend, - pyhf.tensor.tensorflow_backend, pyhf.tensor.jax_backend, ], - ids=['numpy', 'pytorch', 'tensorflow', 'jax'], + ids=['numpy', 'pytorch', 'jax'], ) @pytest.mark.parametrize( 'optimizer', @@ -64,20 +62,16 @@ def test_minimize(tensorlib, optimizer, do_grad, do_stitch): # no grad, scipy, 64b 'no_grad-scipy-numpy': [0.49998815367220306, 0.9999696999038924], 'no_grad-scipy-pytorch': [0.49998815367220306, 0.9999696999038924], - 'no_grad-scipy-tensorflow': [0.49998865164653106, 0.9999696533705097], 'no_grad-scipy-jax': [0.4999880886490433, 0.9999696971774877], # do grad, scipy, 64b 'do_grad-scipy-pytorch': [0.49998837853531425, 0.9999696648069287], - 'do_grad-scipy-tensorflow': [0.4999883785353142, 0.9999696648069278], 'do_grad-scipy-jax': [0.49998837853531414, 0.9999696648069285], # no grad, minuit, 64b - quite consistent 'no_grad-minuit-numpy': [0.5000493563629738, 1.0000043833598724], 'no_grad-minuit-pytorch': [0.5000493563758468, 1.0000043833508256], - 'no_grad-minuit-tensorflow': [0.5000493563645547, 1.0000043833598657], 'no_grad-minuit-jax': [0.5000493563528641, 1.0000043833614634], # do grad, minuit, 64b 'do_grad-minuit-pytorch': [0.500049321728735, 1.00000441739846], - 'do_grad-minuit-tensorflow': [0.5000492930412292, 1.0000044107437134], 'do_grad-minuit-jax': [0.500049321731032, 1.0000044174002167], }[identifier] @@ -107,9 +101,7 @@ def test_optimizer_mixin_extra_kwargs(optimizer): @pytest.mark.parametrize( 'backend,backend_new', - itertools.permutations( - [('numpy', False), ('pytorch', True), ('tensorflow', True), ('jax', True)], 2 - ), + itertools.permutations([('numpy', False), ('pytorch', True), ('jax', True)], 2), ids=lambda pair: f'{pair[0]}', ) def test_minimize_do_grad_autoconfig(mocker, backend, backend_new): diff --git a/tests/test_public_api.py b/tests/test_public_api.py index 17eb46f0e1..3fad05ba57 100644 --- a/tests/test_public_api.py +++ b/tests/test_public_api.py @@ -17,7 +17,7 @@ def model_setup(backend): return model, data, init_pars -@pytest.mark.parametrize("backend_name", ["numpy", "tensorflow", "pytorch", "PyTorch"]) +@pytest.mark.parametrize("backend_name", ["numpy", "pytorch", "PyTorch"]) def test_set_backend_by_string(backend_name): pyhf.set_backend(backend_name) assert isinstance( @@ -43,7 +43,7 @@ def test_set_precision_by_string(precision_level): assert pyhf.tensorlib.precision == precision_level.lower() -@pytest.mark.parametrize("backend_name", [b"numpy", b"tensorflow", b"pytorch"]) +@pytest.mark.parametrize("backend_name", [b"numpy", b"pytorch"]) def test_set_backend_by_bytestring(backend_name): pyhf.set_backend(backend_name) assert isinstance( @@ -143,14 +143,14 @@ def __init__(self, **kwargs): assert pyhf.optimizer.name == optimizer.name -@pytest.mark.parametrize("backend_name", ["numpy", "tensorflow", "pytorch", "PyTorch"]) +@pytest.mark.parametrize("backend_name", ["numpy", "pytorch", "PyTorch"]) def test_backend_no_custom_attributes(backend_name): pyhf.set_backend(backend_name) with pytest.raises(AttributeError): pyhf.tensorlib.nonslotted = True -@pytest.mark.parametrize("backend_name", ["numpy", "tensorflow", "pytorch", "PyTorch"]) +@pytest.mark.parametrize("backend_name", ["numpy", "pytorch", "PyTorch"]) def test_backend_slotted_attributes(backend_name): pyhf.set_backend(backend_name) for attr in ["name", "precision", "dtypemap", "default_do_grad"]: diff --git a/tests/test_scripts.py b/tests/test_scripts.py index 98ebd4ef10..1d9f6075d1 100644 --- a/tests/test_scripts.py +++ b/tests/test_scripts.py @@ -192,7 +192,7 @@ def test_import_usingMounts_badDelimitedPaths(datadir, tmp_path, script_runner): assert 'is not a valid colon-separated option' in ret.stderr -@pytest.mark.parametrize("backend", ["numpy", "tensorflow", "pytorch", "jax"]) +@pytest.mark.parametrize("backend", ["numpy", "pytorch", "jax"]) def test_fit_backend_option(tmp_path, script_runner, backend): temp = tmp_path.joinpath("parsed_output.json") command = f"pyhf xml2json validation/xmlimport_input/config/example.xml --basedir validation/xmlimport_input/ --output-file {temp}" @@ -207,7 +207,7 @@ def test_fit_backend_option(tmp_path, script_runner, backend): assert "mle_parameters" in ret_json -@pytest.mark.parametrize("backend", ["numpy", "tensorflow", "pytorch", "jax"]) +@pytest.mark.parametrize("backend", ["numpy", "pytorch", "jax"]) def test_cls_backend_option(tmp_path, script_runner, backend): temp = tmp_path.joinpath("parsed_output.json") command = f'pyhf xml2json validation/xmlimport_input/config/example.xml --basedir validation/xmlimport_input/ --output-file {temp}' diff --git a/tests/test_simplemodels.py b/tests/test_simplemodels.py index b7b722a156..29a2be33e6 100644 --- a/tests/test_simplemodels.py +++ b/tests/test_simplemodels.py @@ -40,7 +40,6 @@ def test_uncorrelated_background(backend): # See https://github.com/scikit-hep/pyhf/issues/1654 @pytest.mark.fail_pytorch @pytest.mark.fail_pytorch64 -@pytest.mark.fail_tensorflow @pytest.mark.fail_jax def test_correlated_background_default_backend(default_backend): model = pyhf.simplemodels.correlated_background( @@ -59,7 +58,6 @@ def test_correlated_background_default_backend(default_backend): # See https://github.com/scikit-hep/pyhf/issues/1654 @pytest.mark.fail_pytorch @pytest.mark.fail_pytorch64 -@pytest.mark.fail_tensorflow @pytest.mark.fail_jax def test_uncorrelated_background_default_backend(default_backend): model = pyhf.simplemodels.uncorrelated_background( diff --git a/tests/test_tensor.py b/tests/test_tensor.py index da0875ca41..2b0698e22a 100644 --- a/tests/test_tensor.py +++ b/tests/test_tensor.py @@ -2,7 +2,6 @@ import numpy as np import pytest -import tensorflow as tf import pyhf from pyhf.simplemodels import uncorrelated_background @@ -236,21 +235,13 @@ def test_shape(backend): assert tb.shape(tb.astensor([1.0])) == (1,) assert tb.shape(tb.astensor((1.0, 1.0))) == tb.shape(tb.astensor([1.0, 1.0])) assert tb.shape(tb.astensor((0.0, 0.0))) == tb.shape(tb.astensor([0.0, 0.0])) - with pytest.raises( - (ValueError, RuntimeError, tf.errors.InvalidArgumentError, TypeError) - ): + with pytest.raises((ValueError, RuntimeError, TypeError)): _ = tb.astensor([1, 2]) + tb.astensor([3, 4, 5]) - with pytest.raises( - (ValueError, RuntimeError, tf.errors.InvalidArgumentError, TypeError) - ): + with pytest.raises((ValueError, RuntimeError, TypeError)): _ = tb.astensor([1, 2]) - tb.astensor([3, 4, 5]) - with pytest.raises( - (ValueError, RuntimeError, tf.errors.InvalidArgumentError, TypeError) - ): + with pytest.raises((ValueError, RuntimeError, TypeError)): _ = tb.astensor([1, 2]) < tb.astensor([3, 4, 5]) - with pytest.raises( - (ValueError, RuntimeError, tf.errors.InvalidArgumentError, TypeError) - ): + with pytest.raises((ValueError, RuntimeError, TypeError)): _ = tb.astensor([1, 2]) > tb.astensor([3, 4, 5]) with pytest.raises((ValueError, RuntimeError, TypeError)): tb.conditional( @@ -405,10 +396,6 @@ def test_tensor_tile(backend): [[10.0, 20.0, 10.0, 20.0, 10.0, 20.0]], ] - if tb.name == 'tensorflow': - with pytest.raises(tf.errors.InvalidArgumentError): - tb.tile(tb.astensor([[[10, 20, 30]]]), (2, 1)) - def test_1D_gather(backend): tb = pyhf.tensorlib @@ -469,15 +456,6 @@ def test_tensor_to_list(backend): assert tb.tolist(tb.astensor([[1], [2], [3], [4]])) == [[1], [2], [3], [4]] -@pytest.mark.only_tensorflow -def test_tensor_list_conversion(backend): - tb = pyhf.tensorlib - # test when a tensor operation is done, but then need to check if this - # doesn't break in session.run - assert tb.tolist(tb.astensor([1, 2, 3, 4])) == [1, 2, 3, 4] - assert tb.tolist([1, 2, 3, 4]) == [1, 2, 3, 4] - - def test_pdf_eval(backend): source = { "binning": [2, -0.5, 1.5], @@ -554,7 +532,7 @@ def test_tensor_precision(backend): @pytest.mark.parametrize( 'tensorlib', - ['numpy_backend', 'jax_backend', 'pytorch_backend', 'tensorflow_backend'], + ['numpy_backend', 'jax_backend', 'pytorch_backend'], ) @pytest.mark.parametrize('precision', ['64b', '32b']) def test_set_tensor_precision(tensorlib, precision): @@ -596,7 +574,7 @@ def test_trigger_tensorlib_changed_precision(mocker): @pytest.mark.parametrize( 'tensorlib', - ['numpy_backend', 'jax_backend', 'pytorch_backend', 'tensorflow_backend'], + ['numpy_backend', 'jax_backend', 'pytorch_backend'], ) @pytest.mark.parametrize('precision', ['64b', '32b']) def test_tensorlib_setup(tensorlib, precision, mocker): diff --git a/tests/test_validation.py b/tests/test_validation.py index 1e3b54392b..6984f4e001 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -996,7 +996,6 @@ def test_shapesys_nuisparfilter_validation(): [ pyhf.tensor.numpy_backend, pyhf.tensor.jax_backend, - pyhf.tensor.tensorflow_backend, pyhf.tensor.pytorch_backend, ], ) @@ -1021,7 +1020,6 @@ def test_optimizer_stitching(backend, optimizer): 'backend', [ pyhf.tensor.jax_backend, - pyhf.tensor.tensorflow_backend, pyhf.tensor.pytorch_backend, ], ) From 8c556655d6b9148ac98f5f29e4acf9f0fa233362 Mon Sep 17 00:00:00 2001 From: Matthew Feickert Date: Tue, 30 Sep 2025 17:53:01 -0600 Subject: [PATCH 14/14] Remove 'TensorFlow' mentions --- CITATION.cff | 2 +- README.rst | 3 +-- docs/outreach.rst | 12 ++++++------ src/pyhf/tensor/manager.py | 2 +- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/CITATION.cff b/CITATION.cff index a9262484db..e09c96f692 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -37,7 +37,7 @@ abstract: | of that statistical model for multi-bin histogram-based analysis and its interval estimation is based on the asymptotic formulas of "Asymptotic formulae for likelihood-based tests of new physics". pyhf supports modern - computational graph libraries such as TensorFlow, PyTorch, and JAX in order + computational graph libraries such as PyTorch and JAX in order to make use of features such as autodifferentiation and GPU acceleration. references: - type: article diff --git a/README.rst b/README.rst index eda8612ae8..7074aca14e 100644 --- a/README.rst +++ b/README.rst @@ -29,7 +29,7 @@ on the asymptotic formulas of “Asymptotic formulae for likelihood-based tests of new physics” [`arXiv:1007.1727 `__]. The aim is also to support modern computational graph libraries such as PyTorch and -TensorFlow in order to make use of features such as autodifferentiation +JAX in order to make use of features such as autodifferentiation and GPU acceleration. .. @@ -145,7 +145,6 @@ Implemented variations: Computational Backends: - ☑ NumPy - ☑ PyTorch - - ☑ TensorFlow - ☑ JAX Optimizers: diff --git a/docs/outreach.rst b/docs/outreach.rst index a6c3e33bc2..def357964e 100644 --- a/docs/outreach.rst +++ b/docs/outreach.rst @@ -14,9 +14,9 @@ Abstract histogram-based analysis and its interval estimation is based on the asymptotic formulas of "Asymptotic formulae for likelihood-based tests of new physics" :xref:`arXiv:1007.1727`. - pyhf supports modern computational graph libraries such as TensorFlow, - PyTorch, and JAX in order to make use of features such as - auto-differentiation and GPU acceleration. + pyhf supports modern computational graph libraries such as PyTorch and JAX + in order to make use of features such as auto-differentiation and GPU + acceleration. .. code-block:: latex @@ -30,9 +30,9 @@ Abstract estimation is based on the asymptotic formulas of "Asymptotic formulae for likelihood-based tests of new physics" \href{https://arxiv.org/abs/1007.1727}{[arXiv:1007.1727]}. pyhf - supports modern computational graph libraries such as TensorFlow, - PyTorch, and JAX in order to make use of features such as - auto-differentiation and GPU acceleration. + supports modern computational graph libraries such as PyTorch and JAX + in order to make use of features such as auto-differentiation and GPU + acceleration. Presentations diff --git a/src/pyhf/tensor/manager.py b/src/pyhf/tensor/manager.py index f56c8fa2ab..b9bf33fba5 100644 --- a/src/pyhf/tensor/manager.py +++ b/src/pyhf/tensor/manager.py @@ -77,7 +77,7 @@ def set_backend( '64b' Args: - backend (:obj:`str` or :obj:`bytes` or `pyhf.tensor` backend): One of the supported pyhf backends: NumPy, TensorFlow, PyTorch, and JAX + backend (:obj:`str` or :obj:`bytes` or `pyhf.tensor` backend): One of the supported pyhf backends: NumPy, PyTorch, and JAX custom_optimizer (:obj:`str` or :obj:`bytes` or `pyhf.optimize` optimizer or :obj:`None`): Optional custom optimizer defined by the user precision (:obj:`str` or :obj:`bytes` or :obj:`None`): Floating point precision to use in the backend: ``64b`` or ``32b``. Default is backend dependent. default (:obj:`bool`): Set the backend as the default backend additionally