Repository: GridTools/gt4py
Pull request #1215 (2023-03-17 17:54:44)

build: enhance dependency management and make `scipy` optional.
Added:
- New requirements files for oldest versions and oldest versions with extras.
- Documentation for the semi-automatic dependency management workflow with pip-tools and cog.
- Daily CI badge in the main README file.
- New `ufuncs` module for NativeFunctions.

Changed:
- Update and fix minimum gt4py package dependencies.
- Update and fix minimum development dependencies.
- Rename `docs/development/CI` folder to `docs/development/tools`.
- Small additions to ci-infrastructure documentation.
- Daily CI action now tests several requirements files: more recent, oldest versions and oldest versions with extras.
- Small enhancements to tox.ini, which now uses optional environment variables for overriding some settings like the requirements files used for the test environment.
- Refactor ufuncs and code generation for cartesian-NumPy backend.


havogt: It's a simple change, but with quite heavy maintenance cost, unless we find an easy way to test both cases (scipy, no scipy).

Patch:
diff --git a/.github/workflows/daily-ci.yml b/.github/workflows/daily-ci.yml
index c3a204ab..77ba39a3 100644
--- a/.github/workflows/daily-ci.yml
+++ b/.github/workflows/daily-ci.yml
@@ -5,9 +5,9 @@ on:
   - cron: '0 4 * * *'
 
   ## COMMENTED OUT: only for testing CI action changes
-  ## pull_request:
-  ##   branches:
-  ##   - main
+  # pull_request:
+  #   branches:
+  #   - main
   ## END
 
 jobs:
@@ -17,6 +17,7 @@ jobs:
         python-version: ["3.8", "3.9", "3.10"]
         tox-module-factor: ["cartesian", "eve", "next", "storage"]
         os: ["ubuntu-latest"]
+        requirements-file: ["requirements-dev.txt", "min-requirements-test.txt", "min-extra-requirements-test.txt"]
       fail-fast: false
 
     runs-on: ${{ matrix.os }}
@@ -56,7 +57,6 @@ jobs:
       run: |
         pyversion=${{ matrix.python-version }}
         pyversion_no_dot=${pyversion//./}
-        cp constraints.txt CURRENT-constraints.txt 
         tox run -e requirements-py${pyversion_no_dot}
     # TODO(egparedes): add notification for dependencies updates
     # - name: Check for updated requirements
@@ -102,6 +102,7 @@ jobs:
     - name: Run tests
       env:
         NUM_PROCESSES: auto
+        ENV_REQUIREMENTS_FILE: ${{ matrix.requirements-file }}
       run: |
         tox run --skip-missing-interpreters -m test-${{ matrix.tox-module-factor }}-cpu
     - name: Notify slack
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 660698d4..9d58e2de 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -62,7 +62,7 @@ repos:
   ## version = re.search('black==([0-9\.]*)', open("constraints.txt").read())[1] 
   ## print(f"rev: '{version}'  # version from constraints.txt")
   ##]]]
-  rev: '23.1.0'  # version from constraints.txt
+  rev: '23.3.0'  # version from constraints.txt
   ##[[[end]]]
   hooks:
   - id: black
@@ -97,13 +97,13 @@ repos:
     ##     print(f"- {pkg}==" + str(re.search(f'\n{pkg}==([0-9\.]*)', constraints)[1]))
     ##]]]
     - darglint==1.8.1
-    - flake8-bugbear==23.2.13
+    - flake8-bugbear==23.3.12
     - flake8-builtins==2.1.0
     - flake8-debugger==4.1.2
     - flake8-docstrings==1.7.0
     - flake8-eradicate==1.4.0
     - flake8-mutable==1.2.0
-    - flake8-pyproject==1.2.2
+    - flake8-pyproject==1.2.3
     - pygments==2.14.0
     ##[[[end]]]
     # - flake8-rst-docstrings  # Disabled for now due to random false positives
@@ -139,9 +139,9 @@ repos:
   ## version = re.search('mypy==([0-9\.]*)', open("constraints.txt").read())[1] 
   ## print(f"#========= FROM constraints.txt: v{version} =========")
   ##]]]
-  #========= FROM constraints.txt: v1.0.1 =========
+  #========= FROM constraints.txt: v1.1.1 =========
   ##[[[end]]]
-  rev: v1.0.1  # MUST match version ^^^^ in constraints.txt (if the mirror is up-to-date)
+  rev: v1.1.1  # MUST match version ^^^^ in constraints.txt (if the mirror is up-to-date)
   hooks:
   - id: mypy
     additional_dependencies:  # versions from constraints.txt
@@ -155,29 +155,26 @@ repos:
     ##]]]
     - astunparse==1.6.3
     - attrs==22.2.0
-    - black==23.1.0
+    - black==23.3.0
     - boltons==23.0.0
     - cached-property==1.5.2
     - click==8.1.3
-    - cmake==3.25.2
+    - cmake==3.26.1
     - cytoolz==0.12.1
-    - deepdiff==6.2.3
+    - deepdiff==6.3.0
     - devtools==0.10.0
-    - frozendict==2.3.5
+    - frozendict==2.3.6
     - gridtools-cpp==2.2.3
     - importlib-resources==5.12.0
     - jinja2==3.1.2
     - lark==1.1.5
     - mako==1.2.4
-    - networkx==3.0
     - ninja==1.11.1
     - numpy==1.24.2
     - packaging==23.0
-    - pybind11==2.10.3
-    - scipy==1.10.1
-    - setuptools==67.5.1
+    - pybind11==2.10.4
+    - setuptools==67.6.1
     - tabulate==0.9.0
-    - toolz==0.12.0
     - typing-extensions==4.5.0
     - xxhash==3.0.0
     ##[[[end]]]
diff --git a/README.md b/README.md
index 7c313375..4ef08dd9 100644
--- a/README.md
+++ b/README.md
@@ -3,6 +3,7 @@
 ![license](https://img.shields.io/github/license/GridTools/gt4py)
 [![slack](https://img.shields.io/badge/slack-join-orange?logo=slack&labelColor=3a3a3a)](https://join.slack.com/t/gridtools/shared_invite/zt-1mceuj747-59swuowC3MKAuCFyNAnc1g)
 
+[![Daily CI](https://github.com/GridTools/gt4py/actions/workflows/daily-ci.yml/badge.svg)](https://github.com/GridTools/gt4py/actions/workflows/daily-ci.yml)
 ![test-cartesian](https://github.com/GridTools/gt4py/actions/workflows/test-cartesian.yml/badge.svg?branch=main)
 ![test-next](https://github.com/GridTools/gt4py/actions/workflows/test-next.yml/badge.svg?branch=main)
 ![test-storage](https://github.com/GridTools/gt4py/actions/workflows/test-storage.yml/badge.svg?branch=main)
diff --git a/constraints.txt b/constraints.txt
index ac8b82ef..42ce055e 100644
--- a/constraints.txt
+++ b/constraints.txt
@@ -4,13 +4,13 @@
 #
 #    "tox run -e requirements-common"
 #
-aenum==3.1.11             # via dace
+aenum==3.1.12             # via dace
 alabaster==0.7.13         # via sphinx
 asttokens==2.2.1          # via devtools
 astunparse==1.6.3 ; python_version < "3.9"  # via dace, gt4py (pyproject.toml)
 attrs==22.2.0             # via flake8-bugbear, flake8-eradicate, gt4py (pyproject.toml), hypothesis, jsonschema, pytest
 babel==2.12.1             # via sphinx
-black==23.1.0             # via gt4py (pyproject.toml)
+black==23.3.0             # via gt4py (pyproject.toml)
 boltons==23.0.0           # via gt4py (pyproject.toml)
 build==0.10.0             # via pip-tools
 cached-property==1.5.2    # via gt4py (pyproject.toml)
@@ -20,46 +20,46 @@ cffi==1.15.1              # via cryptography
 cfgv==3.3.1               # via pre-commit
 chardet==5.1.0            # via tox
 charset-normalizer==3.1.0  # via requests
-clang-format==15.0.7      # via -r requirements-dev.in, gt4py (pyproject.toml)
+clang-format==16.0.0      # via -r requirements-dev.in, gt4py (pyproject.toml)
 click==8.1.3              # via black, flask, gt4py (pyproject.toml), pip-tools
-cmake==3.25.2             # via -r requirements-dev.in, gt4py (pyproject.toml)
+cmake==3.26.1             # via gt4py (pyproject.toml)
 cogapp==3.3.0             # via -r requirements-dev.in
 colorama==0.4.6           # via tox
-coverage==7.2.1           # via -r requirements-dev.in, pytest-cov
-cryptography==39.0.2      # via types-paramiko, types-pyopenssl, types-redis
+coverage==7.2.2           # via -r requirements-dev.in, pytest-cov
+cryptography==40.0.1      # via types-paramiko, types-pyopenssl, types-redis
 cytoolz==0.12.1           # via gt4py (pyproject.toml)
 dace==0.14.2              # via gt4py (pyproject.toml)
 darglint==1.8.1           # via -r requirements-dev.in
-deepdiff==6.2.3           # via gt4py (pyproject.toml)
-devtools==0.10.0          # via -r requirements-dev.in, gt4py (pyproject.toml)
+deepdiff==6.3.0           # via gt4py (pyproject.toml)
+devtools==0.10.0          # via gt4py (pyproject.toml)
 dill==0.3.6               # via dace
 distlib==0.3.6            # via virtualenv
 docutils==0.18.1          # via restructuredtext-lint, sphinx, sphinx-rtd-theme
 eradicate==2.2.0          # via flake8-eradicate
-exceptiongroup==1.1.0     # via hypothesis, pytest
+exceptiongroup==1.1.1     # via hypothesis, pytest
 execnet==1.9.0            # via pytest-cache, pytest-xdist
 executing==1.2.0          # via devtools
 factory-boy==3.2.1        # via -r requirements-dev.in, pytest-factoryboy
-faker==17.6.0             # via factory-boy
+faker==18.3.4             # via factory-boy
 fastjsonschema==2.16.3    # via nbformat
-filelock==3.9.0           # via tox, virtualenv
+filelock==3.10.7          # via tox, virtualenv
 flake8==5.0.4             # via -r requirements-dev.in, flake8-bugbear, flake8-builtins, flake8-debugger, flake8-docstrings, flake8-eradicate, flake8-mutable, flake8-pyproject, flake8-rst-docstrings
-flake8-bugbear==23.2.13   # via -r requirements-dev.in
+flake8-bugbear==23.3.12   # via -r requirements-dev.in
 flake8-builtins==2.1.0    # via -r requirements-dev.in
 flake8-debugger==4.1.2    # via -r requirements-dev.in
 flake8-docstrings==1.7.0  # via -r requirements-dev.in
 flake8-eradicate==1.4.0   # via -r requirements-dev.in
 flake8-mutable==1.2.0     # via -r requirements-dev.in
-flake8-pyproject==1.2.2   # via -r requirements-dev.in
+flake8-pyproject==1.2.3   # via -r requirements-dev.in
 flake8-rst-docstrings==0.3.0  # via -r requirements-dev.in
 flask==2.2.3              # via dace
-frozendict==2.3.5         # via gt4py (pyproject.toml)
+frozendict==2.3.6         # via gt4py (pyproject.toml)
 gridtools-cpp==2.2.3      # via gt4py (pyproject.toml)
-hypothesis==6.68.2        # via -r requirements-dev.in, gt4py (pyproject.toml)
-identify==2.5.18          # via pre-commit
+hypothesis==6.70.2        # via -r requirements-dev.in, gt4py (pyproject.toml)
+identify==2.5.22          # via pre-commit
 idna==3.4                 # via requests
 imagesize==1.4.1          # via sphinx
-importlib-metadata==6.0.0  # via flask, sphinx
+importlib-metadata==6.1.0  # via flask, sphinx
 importlib-resources==5.12.0 ; python_version < "3.9"  # via gt4py (pyproject.toml), jsonschema
 inflection==0.5.1         # via pytest-factoryboy
 iniconfig==2.0.0          # via pytest
@@ -67,7 +67,7 @@ isort==5.12.0             # via -r requirements-dev.in
 itsdangerous==2.1.2       # via flask
 jinja2==3.1.2             # via flask, gt4py (pyproject.toml), sphinx
 jsonschema==4.17.3        # via nbformat
-jupyter-core==5.2.0       # via nbformat
+jupyter-core==5.3.0       # via nbformat
 jupytext==1.14.5          # via -r requirements-dev.in
 lark==1.1.5               # via gt4py (pyproject.toml)
 mako==1.2.4               # via gt4py (pyproject.toml)
@@ -76,55 +76,53 @@ markupsafe==2.1.2         # via jinja2, mako, werkzeug
 mccabe==0.7.0             # via flake8
 mdit-py-plugins==0.3.5    # via jupytext
 mdurl==0.1.2              # via markdown-it-py
-mpmath==1.2.1             # via sympy
-mypy==1.0.1               # via -r requirements-dev.in
+mpmath==1.3.0             # via sympy
+mypy==1.1.1               # via -r requirements-dev.in
 mypy-extensions==1.0.0    # via black, mypy
-nbformat==5.7.3           # via jupytext
-networkx==3.0             # via dace, gt4py (pyproject.toml)
+nbformat==5.8.0           # via jupytext
+networkx==3.1             # via dace
 ninja==1.11.1             # via gt4py (pyproject.toml)
 nodeenv==1.7.0            # via pre-commit
-numpy==1.24.2             # via dace, gt4py (pyproject.toml), scipy, types-jack-client
+numpy==1.24.2             # via dace, gt4py (pyproject.toml), types-jack-client
 ordered-set==4.1.0        # via deepdiff
-orjson==3.8.7             # via deepdiff
 packaging==23.0           # via black, build, gt4py (pyproject.toml), pyproject-api, pytest, sphinx, tox
-pathspec==0.11.0          # via black
+pathspec==0.11.1          # via black
 pip-tools==6.12.3         # via -r requirements-dev.in
-pipdeptree==2.5.2         # via -r requirements-dev.in
+pipdeptree==2.7.0         # via -r requirements-dev.in
 pkgutil-resolve-name==1.3.10  # via jsonschema
-platformdirs==3.1.0       # via black, jupyter-core, tox, virtualenv
+platformdirs==3.2.0       # via black, jupyter-core, tox, virtualenv
 pluggy==1.0.0             # via pytest, tox
 ply==3.11                 # via dace
-pre-commit==3.1.1         # via -r requirements-dev.in
+pre-commit==3.2.2         # via -r requirements-dev.in
 psutil==5.9.4             # via -r requirements-dev.in, pytest-xdist
-pybind11==2.10.3          # via gt4py (pyproject.toml)
+pybind11==2.10.4          # via gt4py (pyproject.toml)
 pycodestyle==2.9.1        # via flake8, flake8-debugger
 pycparser==2.21           # via cffi
 pydocstyle==6.3.0         # via flake8-docstrings
 pyflakes==2.5.0           # via flake8
 pygments==2.14.0          # via -r requirements-dev.in, flake8-rst-docstrings, sphinx
-pyproject-api==1.5.0      # via tox
+pyproject-api==1.5.1      # via tox
 pyproject-hooks==1.0.0    # via build
 pyrsistent==0.19.3        # via jsonschema
 pytest==7.2.2             # via -r requirements-dev.in, gt4py (pyproject.toml), pytest-cache, pytest-cov, pytest-factoryboy, pytest-xdist
 pytest-cache==1.0         # via -r requirements-dev.in
 pytest-cov==4.0.0         # via -r requirements-dev.in
 pytest-factoryboy==2.5.1  # via -r requirements-dev.in
-pytest-xdist==3.2.0       # via -r requirements-dev.in
+pytest-xdist==3.2.1       # via -r requirements-dev.in
 python-dateutil==2.8.2    # via faker
-pytz==2022.7.1            # via babel
+pytz==2023.3              # via babel
 pyyaml==6.0               # via dace, jupytext, pre-commit
 requests==2.28.2          # via dace, sphinx
 restructuredtext-lint==1.4.0  # via flake8-rst-docstrings
-scipy==1.10.1             # via gt4py (pyproject.toml)
 six==1.16.0               # via asttokens, astunparse, python-dateutil
 snowballstemmer==2.2.0    # via pydocstyle, sphinx
 sortedcontainers==2.4.0   # via hypothesis
-sphinx==6.1.3             # via -r requirements-dev.in, sphinx-rtd-theme
+sphinx==6.1.3             # via -r requirements-dev.in, sphinx-rtd-theme, sphinxcontrib-jquery
 sphinx-rtd-theme==1.2.0   # via -r requirements-dev.in
 sphinxcontrib-applehelp==1.0.4  # via sphinx
 sphinxcontrib-devhelp==1.0.2  # via sphinx
 sphinxcontrib-htmlhelp==2.0.1  # via sphinx
-sphinxcontrib-jquery==2.0.0  # via sphinx-rtd-theme
+sphinxcontrib-jquery==4.1  # via sphinx-rtd-theme
 sphinxcontrib-jsmath==1.0.1  # via sphinx
 sphinxcontrib-qthelp==1.0.3  # via sphinx
 sphinxcontrib-serializinghtml==1.1.5  # via sphinx
@@ -132,36 +130,36 @@ sympy==1.9                # via dace, gt4py (pyproject.toml)
 tabulate==0.9.0           # via gt4py (pyproject.toml)
 toml==0.10.2              # via jupytext
 tomli==2.0.1              # via -r requirements-dev.in, black, build, coverage, flake8-pyproject, mypy, pyproject-api, pyproject-hooks, pytest, tox
-toolz==0.12.0             # via cytoolz, gt4py (pyproject.toml)
-tox==4.4.6                # via -r requirements-dev.in
+toolz==0.12.0             # via cytoolz
+tox==4.4.8                # via -r requirements-dev.in
 traitlets==5.9.0          # via jupyter-core, nbformat
-types-aiofiles==22.1.0.9  # via types-all
+types-aiofiles==23.1.0.1  # via types-all
 types-all==1.0.0          # via -r requirements-dev.in
-types-annoy==1.17.8.2     # via types-all
+types-annoy==1.17.8.3     # via types-all
 types-atomicwrites==1.4.5.1  # via types-all
 types-backports==0.1.3    # via types-all
 types-backports-abc==0.5.2  # via types-all
-types-bleach==6.0.0.1     # via types-all
-types-boto==2.49.18.6     # via types-all
-types-cachetools==5.3.0.4  # via types-all
+types-bleach==6.0.0.2     # via types-all
+types-boto==2.49.18.7     # via types-all
+types-cachetools==5.3.0.5  # via types-all
 types-certifi==2021.10.8.3  # via types-all
-types-cffi==1.15.1.7      # via types-jack-client
+types-cffi==1.15.1.10     # via types-jack-client
 types-characteristic==14.3.7  # via types-all
-types-chardet==5.0.4.2    # via types-all
+types-chardet==5.0.4.3    # via types-all
 types-click==7.1.8        # via types-all, types-flask
-types-click-spinner==0.1.13.3  # via types-all
-types-colorama==0.4.15.8  # via types-all
-types-contextvars==2.4.7.1  # via types-all
-types-croniter==1.3.2.6   # via types-all
+types-click-spinner==0.1.13.4  # via types-all
+types-colorama==0.4.15.11  # via types-all
+types-contextvars==2.4.7.2  # via types-all
+types-croniter==1.3.2.7   # via types-all
 types-cryptography==3.3.23.2  # via types-all, types-openssl-python, types-pyjwt
 types-dataclasses==0.6.6  # via types-all
-types-dateparser==1.1.4.8  # via types-all
-types-datetimerange==2.0.0.3  # via types-all
+types-dateparser==1.1.4.9  # via types-all
+types-datetimerange==2.0.0.5  # via types-all
 types-decorator==5.1.8.3  # via types-all
-types-deprecated==1.2.9.1  # via types-all
+types-deprecated==1.2.9.2  # via types-all
 types-docopt==0.6.11.3    # via types-all
-types-docutils==0.19.1.6  # via types-all
-types-emoji==2.1.0.2      # via types-all
+types-docutils==0.19.1.7  # via types-all
+types-emoji==2.1.0.3      # via types-all
 types-enum34==1.1.8       # via types-all
 types-fb303==1.0.0        # via types-all, types-scribe
 types-filelock==3.2.7     # via types-all
@@ -173,66 +171,66 @@ types-futures==3.3.8      # via types-all
 types-geoip2==3.0.0       # via types-all
 types-ipaddress==1.0.8    # via types-all, types-maxminddb
 types-itsdangerous==1.1.6  # via types-all
-types-jack-client==0.5.10.7  # via types-all
+types-jack-client==0.5.10.8  # via types-all
 types-jinja2==2.11.9      # via types-all, types-flask
 types-kazoo==0.1.3        # via types-all
-types-markdown==3.4.2.5   # via types-all
+types-markdown==3.4.2.7   # via types-all
 types-markupsafe==1.1.10  # via types-all, types-jinja2
 types-maxminddb==1.5.0    # via types-all, types-geoip2
-types-mock==5.0.0.5       # via types-all
+types-mock==5.0.0.6       # via types-all
 types-mypy-extensions==1.0.0.3  # via types-all
 types-nmap==0.1.6         # via types-all
 types-openssl-python==0.1.3  # via types-all
 types-orjson==3.6.2       # via types-all
-types-paramiko==3.0.0.4   # via types-all, types-pysftp
+types-paramiko==3.0.0.7   # via types-all, types-pysftp
 types-pathlib2==2.3.0     # via types-all
-types-pillow==9.4.0.17    # via types-all
+types-pillow==9.4.0.19    # via types-all
 types-pkg-resources==0.1.3  # via types-all
 types-polib==1.2.0.0      # via types-all
-types-protobuf==4.22.0.0  # via types-all
+types-protobuf==4.22.0.2  # via types-all
 types-pyaudio==0.2.16.6   # via types-all
-types-pycurl==7.45.2.3    # via types-all
+types-pycurl==7.45.2.4    # via types-all
 types-pyfarmhash==0.3.1.1  # via types-all
 types-pyjwt==1.7.1        # via types-all
 types-pymssql==2.1.0      # via types-all
-types-pymysql==1.0.19.5   # via types-all
-types-pyopenssl==23.0.0.4  # via types-redis
-types-pyrfc3339==1.1.1.3  # via types-all
-types-pysftp==0.2.17.2    # via types-all
-types-python-dateutil==2.8.19.10  # via types-all, types-datetimerange
+types-pymysql==1.0.19.6   # via types-all
+types-pyopenssl==23.1.0.1  # via types-redis
+types-pyrfc3339==1.1.1.4  # via types-all
+types-pysftp==0.2.17.5    # via types-all
+types-python-dateutil==2.8.19.12  # via types-all, types-datetimerange
 types-python-gflags==3.1.7.2  # via types-all
-types-python-slugify==8.0.0.1  # via types-all
-types-pytz==2022.7.1.2    # via types-all, types-tzlocal
+types-python-slugify==8.0.0.2  # via types-all
+types-pytz==2023.3.0.0    # via types-all, types-tzlocal
 types-pyvmomi==8.0.0.1    # via types-all
-types-pyyaml==6.0.12.8    # via types-all
-types-redis==4.5.1.4      # via types-all
-types-requests==2.28.11.15  # via types-all
-types-retry==0.9.9.2      # via types-all
+types-pyyaml==6.0.12.9    # via types-all
+types-redis==4.5.4.1      # via types-all
+types-requests==2.28.11.17  # via types-all
+types-retry==0.9.9.3      # via types-all
 types-routes==2.5.0       # via types-all
 types-scribe==2.0.0       # via types-all
-types-simplejson==3.18.0.1  # via types-all
+types-simplejson==3.18.0.3  # via types-all
 types-singledispatch==4.0.0.1  # via types-all
-types-six==1.16.21.6      # via types-all
-types-tabulate==0.9.0.1   # via types-all
-types-termcolor==1.1.6.1  # via types-all
-types-toml==0.10.8.5      # via types-all
+types-six==1.16.21.8      # via types-all
+types-tabulate==0.9.0.2   # via types-all
+types-termcolor==1.1.6.2  # via types-all
+types-toml==0.10.8.6      # via types-all
 types-tornado==5.1.1      # via types-all
-types-typed-ast==1.5.8.4  # via types-all
-types-tzlocal==4.2.2.3    # via types-all
+types-typed-ast==1.5.8.6  # via types-all
+types-tzlocal==4.3.0.0    # via types-all
 types-ujson==5.7.0.1      # via types-all
-types-urllib3==1.26.25.8  # via types-requests
-types-waitress==2.1.4.5   # via types-all
+types-urllib3==1.26.25.10  # via types-requests
+types-waitress==2.1.4.7   # via types-all
 types-werkzeug==1.0.9     # via types-all, types-flask
 types-xxhash==3.0.5.2     # via types-all
 typing-extensions==4.5.0  # via black, gt4py (pyproject.toml), mypy, pytest-factoryboy
-urllib3==1.26.14          # via requests
-virtualenv==20.20.0       # via pre-commit, tox
-websockets==10.4          # via dace
+urllib3==1.26.15          # via requests
+virtualenv==20.21.0       # via pre-commit, tox
+websockets==11.0          # via dace
 werkzeug==2.2.3           # via flask
-wheel==0.38.4             # via astunparse, pip-tools
+wheel==0.40.0             # via astunparse, pip-tools
 xxhash==3.0.0             # via gt4py (pyproject.toml)
 zipp==3.15.0              # via importlib-metadata, importlib-resources
 
 # The following packages are considered to be unsafe in a requirements file:
 pip==23.0.1               # via pip-tools
-setuptools==67.5.1        # via -r requirements-dev.in, gt4py (pyproject.toml), nodeenv, pip-tools, sphinxcontrib-jquery
+setuptools==67.6.1        # via gt4py (pyproject.toml), nodeenv, pip-tools
diff --git a/docs/development/CI/infrastructure.md b/docs/development/tools/ci-infrastructure.md
similarity index 86%
rename from docs/development/CI/infrastructure.md
rename to docs/development/tools/ci-infrastructure.md
index 0f5ab82a..0870e532 100644
--- a/docs/development/CI/infrastructure.md
+++ b/docs/development/tools/ci-infrastructure.md
@@ -1,8 +1,8 @@
 # CI infrastructure
 
-Any test job that runs on CI is encoded in automation tools like tox and pre-commit and can be run locally instead.
+Any test job that runs on CI is encoded in automation tools like **tox** and **pre-commit** and can be run locally instead.
 
-## Workflows
+## GitHub Workflows
 
 The following workflows are currently active:
 
@@ -85,13 +85,17 @@ flowchart LR
 
 The `Test Eve`, `Test Storage (CPU)`, `Test Cartesian (CPU)`, and `Test Next (CPU)` workflows run the automated tests for the respective subpackages. In all cases only tests are run that do not require the presence of a GPU.
 
-The `Code Quality` workflow runs pre-commit to check code quality requirements through tools like mypy or flake8.
+The `Code Quality` workflow runs pre-commit to check code quality requirements through tools like **mypy** or **flake8**.
 
 Code coverage workflows are currently disabled.
 
 ### When are workflows triggered
 
-The general idea is to run workflows only when needed. In this monorepo structure, this practically means that a set of tests are only run when the associated sources or the sources of a dependency change. For example, eve tests will not be run when only GT4Py sources are changed.
+The general idea is to run workflows only when needed. In this monorepo structure, this practically means that a set of tests are only run when the associated sources or the sources of a dependency change. For example, `eve` tests will not be run when only GT4Py sources are changed. In those cases where some subpackage workflows are not triggered, replacement (always-succeeding) `Fallback:` workflows will be executed instead to satisfy the GitHub merge protection rules.
+
+### Daily CI
+
+There is an extra CI workflow on GitHub scheduled to run daily and testing `main` with different sets of requirements: newest dependencies, lowest dependencies versions and lowest dependencies versions including extras. Failures are accessible in [GitHub web interface](https://github.com/GridTools/gt4py/actions/workflows/daily-ci.yml) and as the 'Daily CI' badge in the main [README.md](../../../README.md) file. Additionally, in case of failure a message _might_ be posted in the [#jenkins](https://app.slack.com/client/T0A5HP547/C0E145U65) channel of the GridTols slack, but those notifications do not work reliably.
 
 ## CSCS-CI
 
diff --git a/docs/development/CI/cscs-ci.md b/docs/development/tools/cscs-ci.md
similarity index 100%
rename from docs/development/CI/cscs-ci.md
rename to docs/development/tools/cscs-ci.md
diff --git a/docs/development/tools/requirements.md b/docs/development/tools/requirements.md
new file mode 100644
index 00000000..a58cf49a
--- /dev/null
+++ b/docs/development/tools/requirements.md
@@ -0,0 +1,27 @@
+# Requirements
+
+The specification of required third-party packages is scattered and partially duplicated across several configuration files used by several tools. Keeping all package requirements in sync manually is challenging and error-prone. Therefore, in this project we use [pip-tools](https://pip-tools.readthedocs.io/en/latest/) and the [cog](https://nedbatchelder.com/code/cog/) file generation tool to avoid inconsistencies.
+
+The following files in this repository contain information about required third-party packages:
+
+- `pyproject.toml`: GT4Py [package configuration](https://peps.python.org/pep-0621/) used by the build backend (`setuptools`). Install dependencies are specified in the _project.dependencies_ and _project.optional-dependencies_ tables.
+- `requirements-dev.in`: [requirements file](https://pip.pypa.io/en/stable/reference/requirements-file-format/) used by **pip**. It contains a list of packages required for the development of GT4Py. Part of its content is generated automatically from `pyproject.toml` using **cog**.
+- `requirements-dev.txt`: requirements file used by **pip**. It contains a completely frozen list of all packages required for installing and developing GT4Py. It is used by **pip** and **tox** to initialize the standard development and testing environments. It is automatically generated automatically from `requirements-dev.in` by **pip-compile**, when running the **tox** environment to update requirements.
+- `constraints.txt`: [constraints file](https://pip.pypa.io/en/stable/user_guide/#constraints-files) used by **pip** and **tox** to initialize a subset of the standard development environment making sure that if other packages are installed, transitive dependencies are taken from the frozen package list. It is generated automatically from `requirements-dev.in` using **pip-compile**.
+- `min-requirements-test.txt`: requirements file used by **pip**. It contains the minimum list of requirements to run GT4Py tests with the oldest compatible versions of all dependencies. It is generated automatically from `pyproject.toml` using **cog**.
+- `min-extra-requirements-test.txt`: requirements file used by **pip**. It contains the minimum list of requirements to run GT4Py tests with the oldest compatible versions of all dependencies, additionally including all GT4Py extras. It is generated automatically from `pyproject.toml` using **cog**.
+- `.pre-commit-config.yaml`: **pre-commit** configuration with settings for many linting and formatting tools. Part of its content is generated automatically from `pyproject.toml` using **cog**.
+
+The expected workflow to update GT4Py requirements is as follows:
+
+1. For changes in the GT4Py package dependencies, update the relevant table in `pyproject.toml`. When modifying the _project.optional-dependencies_ tables, make sure the `full` extra table **always** contains the combined dependencies from all the other extra tables.
+
+2. For changes in the development tools, update the `requirements-dev.in` file.
+
+3. Run the **tox** _requirements-common_ environment to update all files automatically with **pip-compile** and **cog**. Note that **pip-compile** will most likely update the versions of some unrelated tools if new versions are available in PyPI.
+
+   ```bash
+   tox r -e requirements-common
+   ```
+
+4. Check that the **mypy** mirror used by **pre-commit** (https://github.com/pre-commit/mirrors-mypy) in `.pre-commit-config.yaml` supports the same version as in `constraints.txt`, and manually update the `rev` version number.
diff --git a/pyproject.toml b/pyproject.toml
index a63bdbc2..d7e33633 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,67 +5,64 @@ requires = ["setuptools>=60.6", "wheel", "cython"]
 # ---- Project description ----
 # -- Standard options (PEP 621) --
 [project]
-authors = [{name = "ETH Zurich", email = "gridtools@cscs.ch"}]
+authors = [{name = 'ETH Zurich', email = 'gridtools@cscs.ch'}]
 classifiers = [
-  "Development Status :: 4 - Beta",
-  "Environment :: Console",
-  "Environment :: GPU :: NVIDIA CUDA",
-  "Intended Audience :: Science/Research",
-  "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
-  "Operating System :: POSIX",
-  "Programming Language :: Python",
-  "Programming Language :: Python :: 3.8",
-  "Programming Language :: Python :: 3.9",
-  "Programming Language :: Python :: 3.10",
-  "Programming Language :: Python :: Implementation :: CPython",
-  "Topic :: Scientific/Engineering :: Atmospheric Science",
-  "Topic :: Scientific/Engineering :: Mathematics",
-  "Topic :: Scientific/Engineering :: Physics"
+  'Development Status :: 4 - Beta',
+  'Environment :: Console',
+  'Environment :: GPU :: NVIDIA CUDA',
+  'Intended Audience :: Science/Research',
+  'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)',
+  'Operating System :: POSIX',
+  'Programming Language :: Python',
+  'Programming Language :: Python :: 3.8',
+  'Programming Language :: Python :: 3.9',
+  'Programming Language :: Python :: 3.10',
+  'Programming Language :: Python :: Implementation :: CPython',
+  'Topic :: Scientific/Engineering :: Atmospheric Science',
+  'Topic :: Scientific/Engineering :: Mathematics',
+  'Topic :: Scientific/Engineering :: Physics'
 ]
 dependencies = [
   "astunparse>=1.6.3;python_version<'3.9'",
   "attrs>=21.3",
   "black>=22.3",
-  "boltons>=20.0",
-  "cached-property>=1.5",
-  "click>=7.1",
+  "boltons>=20.1",
+  "cached-property>=1.5.1",
+  "click>=8.0.0",
   "cmake>=3.22",
-  "cytoolz>=0.11",
-  "deepdiff>=5.8",
+  "cytoolz>=0.12.0",
+  "deepdiff>=5.6.0",
   "devtools>=0.6",
   "frozendict>=2.3",
   "gridtools-cpp>=2.2.3,==2.*",
   "importlib-resources>=5.0;python_version<'3.9'",
-  "jinja2>=2.10",
+  "jinja2>=3.0.0",
   "lark>=1.1.2",
   "mako>=1.1",
-  "networkx>=2.4",
   "ninja>=1.10",
-  "numpy>=1.15",
+  "numpy>=1.21.2",
   "packaging>=20.0",
   "pybind11>=2.5",
-  "scipy>=1.4",
-  "setuptools>=60.6",
-  "tabulate>=0.8",
-  "toolz>=0.11",
+  "setuptools>=65.5.0",
+  "tabulate>=0.8.10",
   "typing-extensions>=4.2",
   "xxhash>=1.4.4,<3.1.0"
 ]
-description = "Python library for generating high-performance implementations of stencil kernels for weather and climate modeling from a domain-specific language (DSL)"
-dynamic = ["version"]
+description = 'Python library for generating high-performance implementations of stencil kernels for weather and climate modeling from a domain-specific language (DSL)'
+dynamic = ['version']
 keywords = [
-  "gridtools",
-  "stencil",
-  "weather",
-  "climate",
-  "performance",
-  "portable",
-  "hpc"
+  'gridtools',
+  'stencil',
+  'weather',
+  'climate',
+  'performance',
+  'portable',
+  'hpc'
 ]
-license = {text = "GPL-3.0-or-later"}
-name = "gt4py"
-readme = "README.md"
-requires-python = ">=3.8"
+license = {text = 'GPL-3.0-or-later'}
+name = 'gt4py'
+readme = 'README.md'
+requires-python = '>=3.8'
 
 [project.optional-dependencies]
 cuda = ["cupy"]
@@ -78,9 +75,19 @@ cuda115 = ["cupy-cuda115"]
 cuda116 = ["cupy-cuda116"]
 cuda117 = ["cupy-cuda117"]
 cuda11x = ["cupy-cuda11x"]
-dace = ["dace>=0.14.2,<0.15", "sympy"]
+dace = ["dace>=0.14.2,<0.15", "sympy>=1.7"]
 formatting = ["clang-format>=9.0"]
-testing = ["hypothesis>=4.14", "pytest>=6.1"]
+# Always add all extra packages to 'full' for a simple full gt4py installation
+full = [
+  "clang-format>=9.0",
+  "dace>=0.14.2,<0.15",
+  "hypothesis>=6.0.0",
+  "pytest>=7.0",
+  "sympy>=1.7",
+  "scipy>=1.7.2"
+]
+performance = ["scipy>=1.7.2"]
+testing = ["hypothesis>=6.0.0", "pytest>=7.0"]
 
 [project.scripts]
 gtpyc = "gt4py.cartesian.cli:gtpyc"
@@ -109,8 +116,7 @@ exclude = '''
 '''
 include = '\.pyi?$'
 line-length = 100
-max-line-length = 100
-target_version = ["py310"]
+target-version = ['py310']
 
 # -- coverage --
 [tool.coverage]
@@ -234,99 +240,99 @@ warn_unused_ignores = true
 # GT4Py configs
 [[tool.mypy.overrides]]
 ignore_missing_imports = false
-module = "gt4py.*"
+module = 'gt4py.*'
 
 [[tool.mypy.overrides]]
 # The following ignore_errors are only temporary.
 # TODO: Fix errors and enable these settings.
 disallow_incomplete_defs = false
 disallow_untyped_defs = false
-follow_imports = "silent"
-module = "gt4py.cartesian.*"
+follow_imports = 'silent'
+module = 'gt4py.cartesian.*'
 warn_unused_ignores = false
 
 [[tool.mypy.overrides]]
 ignore_errors = true
-module = "gt4py.cartesian.backend.pyext_builder"
+module = 'gt4py.cartesian.backend.pyext_builder'
 
 [[tool.mypy.overrides]]
 ignore_errors = true
-module = "gt4py.cartesian.frontend.nodes"
+module = 'gt4py.cartesian.frontend.nodes'
 
 [[tool.mypy.overrides]]
 ignore_errors = true
-module = "gt4py.cartesian.frontend.node_util"
+module = 'gt4py.cartesian.frontend.node_util'
 
 [[tool.mypy.overrides]]
 ignore_errors = true
-module = "gt4py.cartesian.frontend.gtscript_frontend"
+module = 'gt4py.cartesian.frontend.gtscript_frontend'
 
 [[tool.mypy.overrides]]
 ignore_errors = true
-module = "gt4py.cartesian.frontend.defir_to_gtir"
+module = 'gt4py.cartesian.frontend.defir_to_gtir'
 
 [[tool.mypy.overrides]]
 ignore_errors = true
-module = "gt4py.cartesian.frontend.meta"
+module = 'gt4py.cartesian.frontend.meta'
 
 [[tool.mypy.overrides]]
 disallow_untyped_defs = true
-module = "gt4py.eve.*"
+module = 'gt4py.eve.*'
 
 [[tool.mypy.overrides]]
-module = "gt4py.eve.extended_typing"
+module = 'gt4py.eve.extended_typing'
 warn_unused_ignores = false
 
 [[tool.mypy.overrides]]
 # TODO: Make this false and fix errors
 allow_untyped_defs = true
-follow_imports = "silent"
-module = "gt4py.storage.*"
+follow_imports = 'silent'
+module = 'gt4py.storage.*'
 warn_unused_ignores = false
 
 [[tool.mypy.overrides]]
 # # TODO: this should be changed to true after a transition period
 disallow_incomplete_defs = false
-module = "gt4py.next.*"
+module = 'gt4py.next.*'
 
 [[tool.mypy.overrides]]
 ignore_errors = true
-module = "gt4py.next.ffront.decorator"
+module = 'gt4py.next.ffront.decorator'
 
 [[tool.mypy.overrides]]
 ignore_errors = true
-module = "gt4py.next.type_system.type_translation"
+module = 'gt4py.next.type_system.type_translation'
 
 [[tool.mypy.overrides]]
 ignore_errors = true
-module = "gt4py.next.iterator.runtime"
+module = 'gt4py.next.iterator.runtime'
 
 [[tool.mypy.overrides]]
 ignore_errors = true
-module = "gt4py.next.iterator.transforms.global_tmps"
+module = 'gt4py.next.iterator.transforms.global_tmps'
 
 # -- pytest --
 [tool.pytest]
 
 [tool.pytest.ini_options]
 markers = [
-  "requires_atlas",  # mark tests that require 'atlas4py' bindings package
-  "requires_dace",  # mark tests that require 'dace' package
-  "requires_gpu:"  # mark tests that require a NVidia GPU (assume 'cupy' and 'cudatoolkit' are installed)
+  'requires_atlas',  # mark tests that require 'atlas4py' bindings package
+  'requires_dace',  # mark tests that require 'dace' package
+  'requires_gpu:'  # mark tests that require a NVidia GPU (assume 'cupy' and 'cudatoolkit' are installed)
 ]
-norecursedirs = ["dist", "build", "cpp_backend_tests/build*", "_local/*", ".*"]
-testpaths = "tests"
+norecursedirs = ['dist', 'build', 'cpp_backend_tests/build*', '_local/*', '.*']
+testpaths = 'tests'
 
 # -- Setuptools-specific --
 [tool.setuptools]
-platforms = ["Linux", "Mac"]
+platforms = ['Linux', 'Mac']
 
 [tool.setuptools.dynamic]
-version = {attr = "gt4py.__about__.__version__"}
+version = {attr = 'gt4py.__about__.__version__'}
 
 [tool.setuptools.package-data]
-"*" = ["*.in", "*.txt"]
-"gt4py" = ["py.typed", "*.md", "*.rst"]
+'*' = ['*.in', '*.txt']
+'gt4py' = ['py.typed', '*.md', '*.rst']
 
 [tool.setuptools.packages]
-find = {namespaces = false, where = ["src"]}
+find = {namespaces = false, where = ['src']}
diff --git a/requirements-dev.in b/requirements-dev.in
index e6856231..53f2ed98 100644
--- a/requirements-dev.in
+++ b/requirements-dev.in
@@ -1,20 +1,28 @@
+##[[[cog
+## import re, tomli
+## project = tomli.loads(open("pyproject.toml").read())
+## versions = "\n".join(project["project"]["optional-dependencies"]["full"])
+## for pkg in ['hypothesis', 'pytest']:
+##    print(re.search(f"\n({pkg} *[=>~!].*)\n", versions)[1])
+##]]]
+hypothesis>=6.0.0
+pytest>=7.0
+##[[[end]]]
+
 clang-format>=9.0
-cmake>=3.18.1
 cogapp>=3.3
 coverage[toml]>=5.0
 darglint>=1.6
-devtools>=0.6
 factory-boy>=3.1
 flake8>=5.0.4
 flake8-bugbear>=20.11.1
 flake8-builtins>=1.5.3
 flake8-debugger>=4.0.0
 flake8-docstrings>=1.5.0
-flake8-eradicate>=1.0.0
+flake8-eradicate>=1.3.0
 flake8-mutable>=1.2.0
 flake8-pyproject>=1.2.2
 flake8-rst-docstrings>=0.0.14
-hypothesis>=4.14
 isort>=5.10
 jupytext>=1.14
 mypy>=1.0
@@ -23,14 +31,12 @@ pip-tools>=6.10
 pre-commit>=2.17
 psutil>=5.0
 pygments>=2.7
-pytest>=6.1
 pytest-cache>=1.0
 pytest-cov>=2.8
-pytest-factoryboy>=2.0
+pytest-factoryboy>=2.0.3
 pytest-xdist[psutil]>=2.4
-setuptools>=40.8.0
 sphinx>=4.4
 sphinx_rtd_theme>=1.0
 tomli>=2.0.1
-tox>=4.2
-types-all
+tox>=3.2.0
+types-all>=1.0.0
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 5ab0b95d..e777e16b 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -4,13 +4,13 @@
 #
 #    "tox run -e requirements-common"
 #
-aenum==3.1.11             # via dace
+aenum==3.1.12             # via dace
 alabaster==0.7.13         # via sphinx
 asttokens==2.2.1          # via devtools
 astunparse==1.6.3 ; python_version < "3.9"  # via dace, gt4py (pyproject.toml)
 attrs==22.2.0             # via flake8-bugbear, flake8-eradicate, gt4py (pyproject.toml), hypothesis, jsonschema, pytest
 babel==2.12.1             # via sphinx
-black==23.1.0             # via gt4py (pyproject.toml)
+black==23.3.0             # via gt4py (pyproject.toml)
 boltons==23.0.0           # via gt4py (pyproject.toml)
 build==0.10.0             # via pip-tools
 cached-property==1.5.2    # via gt4py (pyproject.toml)
@@ -20,46 +20,46 @@ cffi==1.15.1              # via cryptography
 cfgv==3.3.1               # via pre-commit
 chardet==5.1.0            # via tox
 charset-normalizer==3.1.0  # via requests
-clang-format==15.0.7      # via -r requirements-dev.in, gt4py (pyproject.toml)
+clang-format==16.0.0      # via -r requirements-dev.in, gt4py (pyproject.toml)
 click==8.1.3              # via black, flask, gt4py (pyproject.toml), pip-tools
-cmake==3.25.2             # via -r requirements-dev.in, gt4py (pyproject.toml)
+cmake==3.26.1             # via gt4py (pyproject.toml)
 cogapp==3.3.0             # via -r requirements-dev.in
 colorama==0.4.6           # via tox
-coverage[toml]==7.2.1     # via -r requirements-dev.in, pytest-cov
-cryptography==39.0.2      # via types-paramiko, types-pyopenssl, types-redis
+coverage[toml]==7.2.2     # via -r requirements-dev.in, pytest-cov
+cryptography==40.0.1      # via types-paramiko, types-pyopenssl, types-redis
 cytoolz==0.12.1           # via gt4py (pyproject.toml)
 dace==0.14.2              # via gt4py (pyproject.toml)
 darglint==1.8.1           # via -r requirements-dev.in
-deepdiff==6.2.3           # via gt4py (pyproject.toml)
-devtools==0.10.0          # via -r requirements-dev.in, gt4py (pyproject.toml)
+deepdiff==6.3.0           # via gt4py (pyproject.toml)
+devtools==0.10.0          # via gt4py (pyproject.toml)
 dill==0.3.6               # via dace
 distlib==0.3.6            # via virtualenv
 docutils==0.18.1          # via restructuredtext-lint, sphinx, sphinx-rtd-theme
 eradicate==2.2.0          # via flake8-eradicate
-exceptiongroup==1.1.0     # via hypothesis, pytest
+exceptiongroup==1.1.1     # via hypothesis, pytest
 execnet==1.9.0            # via pytest-cache, pytest-xdist
 executing==1.2.0          # via devtools
 factory-boy==3.2.1        # via -r requirements-dev.in, pytest-factoryboy
-faker==17.6.0             # via factory-boy
+faker==18.3.4             # via factory-boy
 fastjsonschema==2.16.3    # via nbformat
-filelock==3.9.0           # via tox, virtualenv
+filelock==3.10.7          # via tox, virtualenv
 flake8==5.0.4             # via -r requirements-dev.in, flake8-bugbear, flake8-builtins, flake8-debugger, flake8-docstrings, flake8-eradicate, flake8-mutable, flake8-pyproject, flake8-rst-docstrings
-flake8-bugbear==23.2.13   # via -r requirements-dev.in
+flake8-bugbear==23.3.12   # via -r requirements-dev.in
 flake8-builtins==2.1.0    # via -r requirements-dev.in
 flake8-debugger==4.1.2    # via -r requirements-dev.in
 flake8-docstrings==1.7.0  # via -r requirements-dev.in
 flake8-eradicate==1.4.0   # via -r requirements-dev.in
 flake8-mutable==1.2.0     # via -r requirements-dev.in
-flake8-pyproject==1.2.2   # via -r requirements-dev.in
+flake8-pyproject==1.2.3   # via -r requirements-dev.in
 flake8-rst-docstrings==0.3.0  # via -r requirements-dev.in
 flask==2.2.3              # via dace
-frozendict==2.3.5         # via gt4py (pyproject.toml)
+frozendict==2.3.6         # via gt4py (pyproject.toml)
 gridtools-cpp==2.2.3      # via gt4py (pyproject.toml)
-hypothesis==6.68.2        # via -r requirements-dev.in, gt4py (pyproject.toml)
-identify==2.5.18          # via pre-commit
+hypothesis==6.70.2        # via -r requirements-dev.in, gt4py (pyproject.toml)
+identify==2.5.22          # via pre-commit
 idna==3.4                 # via requests
 imagesize==1.4.1          # via sphinx
-importlib-metadata==6.0.0  # via flask, sphinx
+importlib-metadata==6.1.0  # via flask, sphinx
 importlib-resources==5.12.0 ; python_version < "3.9"  # via gt4py (pyproject.toml), jsonschema
 inflection==0.5.1         # via pytest-factoryboy
 iniconfig==2.0.0          # via pytest
@@ -67,7 +67,7 @@ isort==5.12.0             # via -r requirements-dev.in
 itsdangerous==2.1.2       # via flask
 jinja2==3.1.2             # via flask, gt4py (pyproject.toml), sphinx
 jsonschema==4.17.3        # via nbformat
-jupyter-core==5.2.0       # via nbformat
+jupyter-core==5.3.0       # via nbformat
 jupytext==1.14.5          # via -r requirements-dev.in
 lark==1.1.5               # via gt4py (pyproject.toml)
 mako==1.2.4               # via gt4py (pyproject.toml)
@@ -76,55 +76,53 @@ markupsafe==2.1.2         # via jinja2, mako, werkzeug
 mccabe==0.7.0             # via flake8
 mdit-py-plugins==0.3.5    # via jupytext
 mdurl==0.1.2              # via markdown-it-py
-mpmath==1.2.1             # via sympy
-mypy==1.0.1               # via -r requirements-dev.in
+mpmath==1.3.0             # via sympy
+mypy==1.1.1               # via -r requirements-dev.in
 mypy-extensions==1.0.0    # via black, mypy
-nbformat==5.7.3           # via jupytext
-networkx==3.0             # via dace, gt4py (pyproject.toml)
+nbformat==5.8.0           # via jupytext
+networkx==3.1             # via dace
 ninja==1.11.1             # via gt4py (pyproject.toml)
 nodeenv==1.7.0            # via pre-commit
-numpy==1.24.2             # via dace, gt4py (pyproject.toml), scipy, types-jack-client
+numpy==1.24.2             # via dace, gt4py (pyproject.toml), types-jack-client
 ordered-set==4.1.0        # via deepdiff
-orjson==3.8.7             # via deepdiff
 packaging==23.0           # via black, build, gt4py (pyproject.toml), pyproject-api, pytest, sphinx, tox
-pathspec==0.11.0          # via black
+pathspec==0.11.1          # via black
 pip-tools==6.12.3         # via -r requirements-dev.in
-pipdeptree==2.5.2         # via -r requirements-dev.in
+pipdeptree==2.7.0         # via -r requirements-dev.in
 pkgutil-resolve-name==1.3.10  # via jsonschema
-platformdirs==3.1.0       # via black, jupyter-core, tox, virtualenv
+platformdirs==3.2.0       # via black, jupyter-core, tox, virtualenv
 pluggy==1.0.0             # via pytest, tox
 ply==3.11                 # via dace
-pre-commit==3.1.1         # via -r requirements-dev.in
+pre-commit==3.2.2         # via -r requirements-dev.in
 psutil==5.9.4             # via -r requirements-dev.in, pytest-xdist
-pybind11==2.10.3          # via gt4py (pyproject.toml)
+pybind11==2.10.4          # via gt4py (pyproject.toml)
 pycodestyle==2.9.1        # via flake8, flake8-debugger
 pycparser==2.21           # via cffi
 pydocstyle==6.3.0         # via flake8-docstrings
 pyflakes==2.5.0           # via flake8
 pygments==2.14.0          # via -r requirements-dev.in, flake8-rst-docstrings, sphinx
-pyproject-api==1.5.0      # via tox
+pyproject-api==1.5.1      # via tox
 pyproject-hooks==1.0.0    # via build
 pyrsistent==0.19.3        # via jsonschema
 pytest==7.2.2             # via -r requirements-dev.in, gt4py (pyproject.toml), pytest-cache, pytest-cov, pytest-factoryboy, pytest-xdist
 pytest-cache==1.0         # via -r requirements-dev.in
 pytest-cov==4.0.0         # via -r requirements-dev.in
 pytest-factoryboy==2.5.1  # via -r requirements-dev.in
-pytest-xdist[psutil]==3.2.0  # via -r requirements-dev.in
+pytest-xdist[psutil]==3.2.1  # via -r requirements-dev.in
 python-dateutil==2.8.2    # via faker
-pytz==2022.7.1            # via babel
+pytz==2023.3              # via babel
 pyyaml==6.0               # via dace, jupytext, pre-commit
 requests==2.28.2          # via dace, sphinx
 restructuredtext-lint==1.4.0  # via flake8-rst-docstrings
-scipy==1.10.1             # via gt4py (pyproject.toml)
 six==1.16.0               # via asttokens, astunparse, python-dateutil
 snowballstemmer==2.2.0    # via pydocstyle, sphinx
 sortedcontainers==2.4.0   # via hypothesis
-sphinx==6.1.3             # via -r requirements-dev.in, sphinx-rtd-theme
+sphinx==6.1.3             # via -r requirements-dev.in, sphinx-rtd-theme, sphinxcontrib-jquery
 sphinx-rtd-theme==1.2.0   # via -r requirements-dev.in
 sphinxcontrib-applehelp==1.0.4  # via sphinx
 sphinxcontrib-devhelp==1.0.2  # via sphinx
 sphinxcontrib-htmlhelp==2.0.1  # via sphinx
-sphinxcontrib-jquery==2.0.0  # via sphinx-rtd-theme
+sphinxcontrib-jquery==4.1  # via sphinx-rtd-theme
 sphinxcontrib-jsmath==1.0.1  # via sphinx
 sphinxcontrib-qthelp==1.0.3  # via sphinx
 sphinxcontrib-serializinghtml==1.1.5  # via sphinx
@@ -132,36 +130,36 @@ sympy==1.9                # via dace, gt4py (pyproject.toml)
 tabulate==0.9.0           # via gt4py (pyproject.toml)
 toml==0.10.2              # via jupytext
 tomli==2.0.1              # via -r requirements-dev.in, black, build, coverage, flake8-pyproject, mypy, pyproject-api, pyproject-hooks, pytest, tox
-toolz==0.12.0             # via cytoolz, gt4py (pyproject.toml)
-tox==4.4.6                # via -r requirements-dev.in
+toolz==0.12.0             # via cytoolz
+tox==4.4.8                # via -r requirements-dev.in
 traitlets==5.9.0          # via jupyter-core, nbformat
-types-aiofiles==22.1.0.9  # via types-all
+types-aiofiles==23.1.0.1  # via types-all
 types-all==1.0.0          # via -r requirements-dev.in
-types-annoy==1.17.8.2     # via types-all
+types-annoy==1.17.8.3     # via types-all
 types-atomicwrites==1.4.5.1  # via types-all
 types-backports==0.1.3    # via types-all
 types-backports-abc==0.5.2  # via types-all
-types-bleach==6.0.0.1     # via types-all
-types-boto==2.49.18.6     # via types-all
-types-cachetools==5.3.0.4  # via types-all
+types-bleach==6.0.0.2     # via types-all
+types-boto==2.49.18.7     # via types-all
+types-cachetools==5.3.0.5  # via types-all
 types-certifi==2021.10.8.3  # via types-all
-types-cffi==1.15.1.7      # via types-jack-client
+types-cffi==1.15.1.10     # via types-jack-client
 types-characteristic==14.3.7  # via types-all
-types-chardet==5.0.4.2    # via types-all
+types-chardet==5.0.4.3    # via types-all
 types-click==7.1.8        # via types-all, types-flask
-types-click-spinner==0.1.13.3  # via types-all
-types-colorama==0.4.15.8  # via types-all
-types-contextvars==2.4.7.1  # via types-all
-types-croniter==1.3.2.6   # via types-all
+types-click-spinner==0.1.13.4  # via types-all
+types-colorama==0.4.15.11  # via types-all
+types-contextvars==2.4.7.2  # via types-all
+types-croniter==1.3.2.7   # via types-all
 types-cryptography==3.3.23.2  # via types-all, types-openssl-python, types-pyjwt
 types-dataclasses==0.6.6  # via types-all
-types-dateparser==1.1.4.8  # via types-all
-types-datetimerange==2.0.0.3  # via types-all
+types-dateparser==1.1.4.9  # via types-all
+types-datetimerange==2.0.0.5  # via types-all
 types-decorator==5.1.8.3  # via types-all
-types-deprecated==1.2.9.1  # via types-all
+types-deprecated==1.2.9.2  # via types-all
 types-docopt==0.6.11.3    # via types-all
-types-docutils==0.19.1.6  # via types-all
-types-emoji==2.1.0.2      # via types-all
+types-docutils==0.19.1.7  # via types-all
+types-emoji==2.1.0.3      # via types-all
 types-enum34==1.1.8       # via types-all
 types-fb303==1.0.0        # via types-all, types-scribe
 types-filelock==3.2.7     # via types-all
@@ -173,66 +171,66 @@ types-futures==3.3.8      # via types-all
 types-geoip2==3.0.0       # via types-all
 types-ipaddress==1.0.8    # via types-all, types-maxminddb
 types-itsdangerous==1.1.6  # via types-all
-types-jack-client==0.5.10.7  # via types-all
+types-jack-client==0.5.10.8  # via types-all
 types-jinja2==2.11.9      # via types-all, types-flask
 types-kazoo==0.1.3        # via types-all
-types-markdown==3.4.2.5   # via types-all
+types-markdown==3.4.2.7   # via types-all
 types-markupsafe==1.1.10  # via types-all, types-jinja2
 types-maxminddb==1.5.0    # via types-all, types-geoip2
-types-mock==5.0.0.5       # via types-all
+types-mock==5.0.0.6       # via types-all
 types-mypy-extensions==1.0.0.3  # via types-all
 types-nmap==0.1.6         # via types-all
 types-openssl-python==0.1.3  # via types-all
 types-orjson==3.6.2       # via types-all
-types-paramiko==3.0.0.4   # via types-all, types-pysftp
+types-paramiko==3.0.0.7   # via types-all, types-pysftp
 types-pathlib2==2.3.0     # via types-all
-types-pillow==9.4.0.17    # via types-all
+types-pillow==9.4.0.19    # via types-all
 types-pkg-resources==0.1.3  # via types-all
 types-polib==1.2.0.0      # via types-all
-types-protobuf==4.22.0.0  # via types-all
+types-protobuf==4.22.0.2  # via types-all
 types-pyaudio==0.2.16.6   # via types-all
-types-pycurl==7.45.2.3    # via types-all
+types-pycurl==7.45.2.4    # via types-all
 types-pyfarmhash==0.3.1.1  # via types-all
 types-pyjwt==1.7.1        # via types-all
 types-pymssql==2.1.0      # via types-all
-types-pymysql==1.0.19.5   # via types-all
-types-pyopenssl==23.0.0.4  # via types-redis
-types-pyrfc3339==1.1.1.3  # via types-all
-types-pysftp==0.2.17.2    # via types-all
-types-python-dateutil==2.8.19.10  # via types-all, types-datetimerange
+types-pymysql==1.0.19.6   # via types-all
+types-pyopenssl==23.1.0.1  # via types-redis
+types-pyrfc3339==1.1.1.4  # via types-all
+types-pysftp==0.2.17.5    # via types-all
+types-python-dateutil==2.8.19.12  # via types-all, types-datetimerange
 types-python-gflags==3.1.7.2  # via types-all
-types-python-slugify==8.0.0.1  # via types-all
-types-pytz==2022.7.1.2    # via types-all, types-tzlocal
+types-python-slugify==8.0.0.2  # via types-all
+types-pytz==2023.3.0.0    # via types-all, types-tzlocal
 types-pyvmomi==8.0.0.1    # via types-all
-types-pyyaml==6.0.12.8    # via types-all
-types-redis==4.5.1.4      # via types-all
-types-requests==2.28.11.15  # via types-all
-types-retry==0.9.9.2      # via types-all
+types-pyyaml==6.0.12.9    # via types-all
+types-redis==4.5.4.1      # via types-all
+types-requests==2.28.11.17  # via types-all
+types-retry==0.9.9.3      # via types-all
 types-routes==2.5.0       # via types-all
 types-scribe==2.0.0       # via types-all
-types-simplejson==3.18.0.1  # via types-all
+types-simplejson==3.18.0.3  # via types-all
 types-singledispatch==4.0.0.1  # via types-all
-types-six==1.16.21.6      # via types-all
-types-tabulate==0.9.0.1   # via types-all
-types-termcolor==1.1.6.1  # via types-all
-types-toml==0.10.8.5      # via types-all
+types-six==1.16.21.8      # via types-all
+types-tabulate==0.9.0.2   # via types-all
+types-termcolor==1.1.6.2  # via types-all
+types-toml==0.10.8.6      # via types-all
 types-tornado==5.1.1      # via types-all
-types-typed-ast==1.5.8.4  # via types-all
-types-tzlocal==4.2.2.3    # via types-all
+types-typed-ast==1.5.8.6  # via types-all
+types-tzlocal==4.3.0.0    # via types-all
 types-ujson==5.7.0.1      # via types-all
-types-urllib3==1.26.25.8  # via types-requests
-types-waitress==2.1.4.5   # via types-all
+types-urllib3==1.26.25.10  # via types-requests
+types-waitress==2.1.4.7   # via types-all
 types-werkzeug==1.0.9     # via types-all, types-flask
 types-xxhash==3.0.5.2     # via types-all
 typing-extensions==4.5.0  # via black, gt4py (pyproject.toml), mypy, pytest-factoryboy
-urllib3==1.26.14          # via requests
-virtualenv==20.20.0       # via pre-commit, tox
-websockets==10.4          # via dace
+urllib3==1.26.15          # via requests
+virtualenv==20.21.0       # via pre-commit, tox
+websockets==11.0          # via dace
 werkzeug==2.2.3           # via flask
-wheel==0.38.4             # via astunparse, pip-tools
+wheel==0.40.0             # via astunparse, pip-tools
 xxhash==3.0.0             # via gt4py (pyproject.toml)
 zipp==3.15.0              # via importlib-metadata, importlib-resources
 
 # The following packages are considered to be unsafe in a requirements file:
 pip==23.0.1               # via pip-tools
-setuptools==67.5.1        # via -r requirements-dev.in, gt4py (pyproject.toml), nodeenv, pip-tools, sphinxcontrib-jquery
+setuptools==67.6.1        # via gt4py (pyproject.toml), nodeenv, pip-tools
diff --git a/src/gt4py/cartesian/gtc/common.py b/src/gt4py/cartesian/gtc/common.py
index 265c229b..c2a16418 100644
--- a/src/gt4py/cartesian/gtc/common.py
+++ b/src/gt4py/cartesian/gtc/common.py
@@ -17,12 +17,25 @@ from __future__ import annotations
 import enum
 import functools
 import typing
-from typing import Any, ClassVar, Dict, Generic, List, Optional, Tuple, Type, TypeVar, Union
+from typing import (
+    Any,
+    ClassVar,
+    Dict,
+    Final,
+    Generic,
+    List,
+    Mapping,
+    Optional,
+    Tuple,
+    Type,
+    TypeVar,
+    Union,
+)
 
 import numpy as np
-import scipy.special
 
 from gt4py import eve
+from gt4py.cartesian.gtc import ufuncs
 from gt4py.cartesian.gtc.utils import dimension_flags_to_names, flatten_list
 from gt4py.eve import datamodels
 
@@ -808,82 +821,97 @@ def data_type_to_typestr(dtype: DataType) -> str:
     return np.dtype(table[dtype]).str
 
 
-@functools.lru_cache(maxsize=None, typed=True)  # typed since uniqueness is only guaranteed per enum
+# This is a mapping of mappings instead of a flat mapping because
+# different operators use the same key: UnaryOperator.POS == BinaryOperator.ADD
+OP_TO_UFUNC_NAME: Final[
+    Mapping[
+        Union[
+            Type[UnaryOperator],
+            Type[ArithmeticOperator],
+            Type[ComparisonOperator],
+            Type[LogicalOperator],
+            Type[NativeFunction],
+        ],
+        Mapping[
+            Union[
+                UnaryOperator,
+                ArithmeticOperator,
+                ComparisonOperator,
+                LogicalOperator,
+                NativeFunction,
+            ],
+            str,
+        ],
+    ]
+] = {
+    UnaryOperator: {
+        UnaryOperator.POS: "positive",
+        UnaryOperator.NEG: "negative",
+        UnaryOperator.NOT: "logical_not",
+    },
+    ArithmeticOperator: {
+        ArithmeticOperator.ADD: "add",
+        ArithmeticOperator.SUB: "subtract",
+        ArithmeticOperator.MUL: "multiply",
+        ArithmeticOperator.DIV: "true_divide",
+    },
+    ComparisonOperator: {
+        ComparisonOperator.GT: "greater",
+        ComparisonOperator.LT: "less",
+        ComparisonOperator.GE: "greater_equal",
+        ComparisonOperator.LE: "less_equal",
+        ComparisonOperator.EQ: "equal",
+        ComparisonOperator.NE: "not_equal",
+    },
+    LogicalOperator: {
+        LogicalOperator.AND: "logical_and",
+        LogicalOperator.OR: "logical_or",
+    },
+    NativeFunction: {
+        NativeFunction.ABS: "abs",
+        NativeFunction.MIN: "minimum",
+        NativeFunction.MAX: "maximum",
+        NativeFunction.MOD: "remainder",
+        NativeFunction.SIN: "sin",
+        NativeFunction.COS: "cos",
+        NativeFunction.TAN: "tan",
+        NativeFunction.ARCSIN: "arcsin",
+        NativeFunction.ARCCOS: "arccos",
+        NativeFunction.ARCTAN: "arctan",
+        NativeFunction.SINH: "sinh",
+        NativeFunction.COSH: "cosh",
+        NativeFunction.TANH: "tanh",
+        NativeFunction.ARCSINH: "arcsinh",
+        NativeFunction.ARCCOSH: "arccosh",
+        NativeFunction.ARCTANH: "arctanh",
+        NativeFunction.SQRT: "sqrt",
+        NativeFunction.POW: "power",
+        NativeFunction.EXP: "exp",
+        NativeFunction.LOG: "log",
+        NativeFunction.GAMMA: "gamma",
+        NativeFunction.CBRT: "cbrt",
+        NativeFunction.ISFINITE: "isfinite",
+        NativeFunction.ISINF: "isinf",
+        NativeFunction.ISNAN: "isnan",
+        NativeFunction.FLOOR: "floor",
+        NativeFunction.CEIL: "ceil",
+        NativeFunction.TRUNC: "trunc",
+    },
+}
+
+
 def op_to_ufunc(
     op: Union[
         UnaryOperator, ArithmeticOperator, ComparisonOperator, LogicalOperator, NativeFunction
     ]
 ) -> np.ufunc:
-    table: Dict[
-        Union[
-            UnaryOperator, ArithmeticOperator, ComparisonOperator, LogicalOperator, NativeFunction
-        ],
-        np.ufunc,
-    ]
-    # Can't put all in single table since UnaryOperator.POS == BinaryOperator.ADD
-    if isinstance(op, UnaryOperator):
-        table = {
-            UnaryOperator.POS: np.positive,
-            UnaryOperator.NEG: np.negative,
-            UnaryOperator.NOT: np.logical_not,
-        }
-    elif isinstance(op, ArithmeticOperator):
-        table = {
-            ArithmeticOperator.ADD: np.add,
-            ArithmeticOperator.SUB: np.subtract,
-            ArithmeticOperator.MUL: np.multiply,
-            ArithmeticOperator.DIV: np.true_divide,
-        }
-    elif isinstance(op, ComparisonOperator):
-        table = {
-            ComparisonOperator.GT: np.greater,
-            ComparisonOperator.LT: np.less,
-            ComparisonOperator.GE: np.greater_equal,
-            ComparisonOperator.LE: np.less_equal,
-            ComparisonOperator.EQ: np.equal,
-            ComparisonOperator.NE: np.not_equal,
-        }
-    elif isinstance(op, LogicalOperator):
-        table = {
-            LogicalOperator.AND: np.logical_and,
-            LogicalOperator.OR: np.logical_or,
-        }
-    elif isinstance(op, NativeFunction):
-        table = {
-            NativeFunction.ABS: np.abs,
-            NativeFunction.MIN: np.minimum,
-            NativeFunction.MAX: np.maximum,
-            NativeFunction.MOD: np.remainder,
-            NativeFunction.SIN: np.sin,
-            NativeFunction.COS: np.cos,
-            NativeFunction.TAN: np.tan,
-            NativeFunction.ARCSIN: np.arcsin,
-            NativeFunction.ARCCOS: np.arccos,
-            NativeFunction.ARCTAN: np.arctan,
-            NativeFunction.SINH: np.sinh,
-            NativeFunction.COSH: np.cosh,
-            NativeFunction.TANH: np.tanh,
-            NativeFunction.ARCSINH: np.arcsinh,
-            NativeFunction.ARCCOSH: np.arccosh,
-            NativeFunction.ARCTANH: np.arctanh,
-            NativeFunction.SQRT: np.sqrt,
-            NativeFunction.POW: np.power,
-            NativeFunction.EXP: np.exp,
-            NativeFunction.LOG: np.log,
-            NativeFunction.GAMMA: scipy.special.gamma,
-            NativeFunction.CBRT: np.cbrt,
-            NativeFunction.ISFINITE: np.isfinite,
-            NativeFunction.ISINF: np.isinf,
-            NativeFunction.ISNAN: np.isnan,
-            NativeFunction.FLOOR: np.floor,
-            NativeFunction.CEIL: np.ceil,
-            NativeFunction.TRUNC: np.trunc,
-        }
-    else:
+    if not isinstance(
+        op, (UnaryOperator, ArithmeticOperator, ComparisonOperator, LogicalOperator, NativeFunction)
+    ):
         raise TypeError(
             "Can only convert instances of GTC operators and supported native functions to typestr."
         )
-    return table[op]
+    return getattr(ufuncs, OP_TO_UFUNC_NAME[type(op)][op])
 
 
 @functools.lru_cache(maxsize=None)
diff --git a/src/gt4py/cartesian/gtc/numpy/npir_codegen.py b/src/gt4py/cartesian/gtc/numpy/npir_codegen.py
index a22f56bd..cf44ed41 100644
--- a/src/gt4py/cartesian/gtc/numpy/npir_codegen.py
+++ b/src/gt4py/cartesian/gtc/numpy/npir_codegen.py
@@ -244,17 +244,7 @@ class NpirCodegen(codegen.TemplatedGenerator, eve.VisitorWithSymbolTableTrait):
     def visit_NativeFunction(
         self, node: common.NativeFunction, **kwargs: Any
     ) -> Union[str, Collection[str]]:
-        if node == common.NativeFunction.MIN:
-            return "np.minimum"
-        elif node == common.NativeFunction.MAX:
-            return "np.maximum"
-        elif node == common.NativeFunction.POW:
-            return "np.power"
-        elif node == common.NativeFunction.GAMMA:
-            return "scipy.special.gamma"
-        name = self.generic_visit(node, **kwargs)
-        assert isinstance(name, str)
-        return f"np.{name}"
+        return f"ufuncs.{common.OP_TO_UFUNC_NAME[common.NativeFunction][node]}"
 
     def visit_NativeFuncCall(
         self, node: npir.NativeFuncCall, *, mask: Optional[str] = None, **kwargs: Any
@@ -398,7 +388,7 @@ class NpirCodegen(codegen.TemplatedGenerator, eve.VisitorWithSymbolTableTrait):
             from typing import Tuple
 
             import numpy as np
-            import scipy.special
+            from gt4py.cartesian.gtc import ufuncs
 
             {{ data_view_class }}
 
diff --git a/src/gt4py/cartesian/gtc/ufuncs.py b/src/gt4py/cartesian/gtc/ufuncs.py
new file mode 100644
index 00000000..980abe6b
--- /dev/null
+++ b/src/gt4py/cartesian/gtc/ufuncs.py
@@ -0,0 +1,70 @@
+# GT4Py - GridTools Framework
+#
+# Copyright (c) 2014-2023, ETH Zurich
+# All rights reserved.
+#
+# This file is part of the GT4Py project and the GridTools framework.
+# GT4Py is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the
+# Free Software Foundation, either version 3 of the License, or any later
+# version. See the LICENSE.txt file at the top-level directory of this
+# distribution for a copy of the license or check <https://www.gnu.org/licenses/>.
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+import numpy as np
+
+
+try:
+    from scipy.special import gamma as gamma_
+except ImportError:
+    import math
+
+    # If scipy is not available, emulate gamma function using math.gamma
+    gamma_ = np.vectorize(math.gamma)
+    gamma_.types = ["f->f", "d->d", "F->F", "D->D"]
+
+
+positive: np.ufunc = np.positive
+negative: np.ufunc = np.negative
+logical_not: np.ufunc = np.logical_not
+add: np.ufunc = np.add
+subtract: np.ufunc = np.subtract
+multiply: np.ufunc = np.multiply
+true_divide: np.ufunc = np.true_divide
+greater: np.ufunc = np.greater
+less: np.ufunc = np.less
+greater_equal: np.ufunc = np.greater_equal
+less_equal: np.ufunc = np.less_equal
+equal: np.ufunc = np.equal
+not_equal: np.ufunc = np.not_equal
+logical_and: np.ufunc = np.logical_and
+logical_or: np.ufunc = np.logical_or
+abs: np.ufunc = np.abs  # noqa: A001  # shadowing abs builtin
+minimum: np.ufunc = np.minimum
+maximum: np.ufunc = np.maximum
+remainder: np.ufunc = np.remainder
+sin: np.ufunc = np.sin
+cos: np.ufunc = np.cos
+tan: np.ufunc = np.tan
+arcsin: np.ufunc = np.arcsin
+arccos: np.ufunc = np.arccos
+arctan: np.ufunc = np.arctan
+sinh: np.ufunc = np.sinh
+cosh: np.ufunc = np.cosh
+tanh: np.ufunc = np.tanh
+arcsinh: np.ufunc = np.arcsinh
+arccosh: np.ufunc = np.arccosh
+arctanh: np.ufunc = np.arctanh
+sqrt: np.ufunc = np.sqrt
+power: np.ufunc = np.power
+exp: np.ufunc = np.exp
+log: np.ufunc = np.log
+gamma: np.ufunc = gamma_
+cbrt: np.ufunc = np.cbrt
+isfinite: np.ufunc = np.isfinite
+isinf: np.ufunc = np.isinf
+isnan: np.ufunc = np.isnan
+floor: np.ufunc = np.floor
+ceil: np.ufunc = np.ceil
+trunc: np.ufunc = np.trunc
diff --git a/src/gt4py/next/iterator/ir.py b/src/gt4py/next/iterator/ir.py
index 716cdf1d..ede08d63 100644
--- a/src/gt4py/next/iterator/ir.py
+++ b/src/gt4py/next/iterator/ir.py
@@ -205,15 +205,15 @@ class FencilDefinition(Node, ValidatedSymbolTableTrait):
 
 
 # TODO(fthaler): just use hashable types in nodes (tuples instead of lists)
-Sym.__hash__ = Node.__hash__  # type: ignore[assignment]
-Expr.__hash__ = Node.__hash__  # type: ignore[assignment]
-Literal.__hash__ = Node.__hash__  # type: ignore[assignment]
-NoneLiteral.__hash__ = Node.__hash__  # type: ignore[assignment]
-OffsetLiteral.__hash__ = Node.__hash__  # type: ignore[assignment]
-AxisLiteral.__hash__ = Node.__hash__  # type: ignore[assignment]
-SymRef.__hash__ = Node.__hash__  # type: ignore[assignment]
-Lambda.__hash__ = Node.__hash__  # type: ignore[assignment]
-FunCall.__hash__ = Node.__hash__  # type: ignore[assignment]
-FunctionDefinition.__hash__ = Node.__hash__  # type: ignore[assignment]
-StencilClosure.__hash__ = Node.__hash__  # type: ignore[assignment]
-FencilDefinition.__hash__ = Node.__hash__  # type: ignore[assignment]
+Sym.__hash__ = Node.__hash__  # type: ignore[method-assign]
+Expr.__hash__ = Node.__hash__  # type: ignore[method-assign]
+Literal.__hash__ = Node.__hash__  # type: ignore[method-assign]
+NoneLiteral.__hash__ = Node.__hash__  # type: ignore[method-assign]
+OffsetLiteral.__hash__ = Node.__hash__  # type: ignore[method-assign]
+AxisLiteral.__hash__ = Node.__hash__  # type: ignore[method-assign]
+SymRef.__hash__ = Node.__hash__  # type: ignore[method-assign]
+Lambda.__hash__ = Node.__hash__  # type: ignore[method-assign]
+FunCall.__hash__ = Node.__hash__  # type: ignore[method-assign]
+FunctionDefinition.__hash__ = Node.__hash__  # type: ignore[method-assign]
+StencilClosure.__hash__ = Node.__hash__  # type: ignore[method-assign]
+FencilDefinition.__hash__ = Node.__hash__  # type: ignore[method-assign]
diff --git a/src/gt4py/next/otf/workflow.py b/src/gt4py/next/otf/workflow.py
index bde64287..6420a4dd 100644
--- a/src/gt4py/next/otf/workflow.py
+++ b/src/gt4py/next/otf/workflow.py
@@ -79,8 +79,9 @@ class ReplaceEnabledWorkflowMixin(Workflow[StartT_contra, EndT_co], Protocol):
         Raises:
             TypeError: If `self` is not a dataclass.
         """
-        if not dataclasses.is_dataclass(self.__class__):
+        if not dataclasses.is_dataclass(self):
             raise TypeError(f"{self.__class__} is not a dataclass")
+        assert not isinstance(self, type)
         return dataclasses.replace(self, **kwargs)
 
 
diff --git a/tox.ini b/tox.ini
index 0b54a2b0..730d8dca 100644
--- a/tox.ini
+++ b/tox.ini
@@ -28,7 +28,7 @@ labels =
     storage-py38-dace-cpu, storage-py39-dace-cpu, storage-py310-dace-cpu
 
 [testenv]
-deps = -r {tox_root}{/}requirements-dev.txt
+deps = -r {tox_root}{/}{env:ENV_REQUIREMENTS_FILE:requirements-dev.txt}
 constrain_package_deps = true
 use_frozen_constraints = true
 extras =
@@ -49,7 +49,7 @@ package = wheel
 wheel_build_env = .pkg
 pass_env = NUM_PROCESSES
 set_env =
-    PYTHONWARNINGS = ignore:Support for `[tool.setuptools]` in `pyproject.toml` is still *beta*:UserWarning
+    PYTHONWARNINGS = {env:PYTHONWARNINGS:ignore:Support for `[tool.setuptools]` in `pyproject.toml` is still *beta*:UserWarning}
 
 [testenv:cartesian-py{38,39,310}-{internal,dace}-{cpu,cuda,cuda110,cuda111,cuda112,cuda113,cuda114,cuda115,cuda116,cuda117,cuda11x}]
 description = Run 'gt4py.cartesian' tests
@@ -72,6 +72,12 @@ commands =
 ;    coverage json --rcfile=setup.cfg
 ;    coverage html --rcfile=setup.cfg --show-contexts
 
+[testenv:eve-py{38,39,310}]
+description = Run 'gt4py.eve' tests
+commands =
+    python -m pytest --cache-clear -v -n {env:NUM_PROCESSES:1} {posargs} tests{/}eve_tests
+    python -m pytest --doctest-modules src{/}gt4py{/}eve
+
 [testenv:next-py{310}-{nomesh,atlas}]
 description = Run 'gt4py.next' tests
 pass_env = {[testenv]pass_env}, BOOST_ROOT, BOOST_HOME, CUDA_HOME, CUDA_PATH
@@ -86,12 +92,6 @@ commands =
     atlas: python -m pytest --cache-clear -v -n {env:NUM_PROCESSES:1} -m "requires_atlas" {posargs} tests{/}next_tests
     pytest --doctest-modules src{/}gt4py{/}next
 
-[testenv:eve-py{38,39,310}]
-description = Run 'gt4py.eve' tests
-commands =
-    python -m pytest --cache-clear -v -n {env:NUM_PROCESSES:1} {posargs} tests{/}eve_tests
-    python -m pytest --doctest-modules src{/}gt4py{/}eve
-
 [testenv:storage-py{38,39,310}-{internal,dace}-{cpu,cuda,cuda110,cuda111,cuda112,cuda113,cuda114,cuda115,cuda116,cuda117,cuda11x}]
 description = Run 'gt4py.storage' tests
 commands =
@@ -155,6 +155,8 @@ allowlist_externals =
 commands =
     -mv constraints.txt constraints.txt.old
     -mv requirements-dev.txt requirements-dev.old
+    # Run cog to update requirements files from pyproject
+    cog -r -P requirements-dev.in min-requirements-test.txt min-extra-requirements-test.txt
     # Generate constraints file removing extras
     # (extras are not supported by pip in constraints files)
     pip-compile -r --resolver=backtracking \
@@ -178,7 +180,6 @@ commands =
     --extra testing \
     -o requirements-dev.txt \
     pyproject.toml requirements-dev.in
-
     # Run cog to update .pre-commit-config.yaml with new versions
     common: cog -r -P .pre-commit-config.yaml
 


Test patch:
diff --git a/min-extra-requirements-test.txt b/min-extra-requirements-test.txt
new file mode 100644
index 00000000..78bc265a
--- /dev/null
+++ b/min-extra-requirements-test.txt
@@ -0,0 +1,77 @@
+##[[[cog
+## import re, tomli
+## project = tomli.loads(open("pyproject.toml").read())
+## requirements = set(
+##     rr
+##     for r in (
+##         project["project"]["dependencies"]
+##         + project["project"]["optional-dependencies"]["full"]
+##         + open("requirements-dev.in").readlines()
+##     )
+##     if (rr := r.strip()) and not rr.startswith('#')
+## )
+## for r in sorted(requirements):
+##     m = re.match("^([\w-][\w\d\[\]-]*)[=>~][=]([^,]+)", r)
+##     print(f"{m[1]}=={m[2].strip()}")
+##]]]
+astunparse==1.6.3;python_version<'3.9'
+attrs==21.3
+black==22.3
+boltons==20.1
+cached-property==1.5.1
+clang-format==9.0
+click==8.0.0
+cmake==3.22
+cogapp==3.3
+coverage[toml]==5.0
+cytoolz==0.12.0
+dace==0.14.2
+darglint==1.6
+deepdiff==5.6.0
+devtools==0.6
+factory-boy==3.1
+flake8-bugbear==20.11.1
+flake8-builtins==1.5.3
+flake8-debugger==4.0.0
+flake8-docstrings==1.5.0
+flake8-eradicate==1.3.0
+flake8-mutable==1.2.0
+flake8-pyproject==1.2.2
+flake8-rst-docstrings==0.0.14
+flake8==5.0.4
+frozendict==2.3
+gridtools-cpp==2.2.3
+hypothesis==6.0.0
+importlib-resources==5.0;python_version<'3.9'
+isort==5.10
+jinja2==3.0.0
+jupytext==1.14
+lark==1.1.2
+mako==1.1
+mypy==1.0
+ninja==1.10
+numpy==1.21.2
+packaging==20.0
+pip-tools==6.10
+pipdeptree==2.3
+pre-commit==2.17
+psutil==5.0
+pybind11==2.5
+pygments==2.7
+pytest-cache==1.0
+pytest-cov==2.8
+pytest-factoryboy==2.0.3
+pytest-xdist[psutil]==2.4
+pytest==7.0
+scipy==1.7.2
+setuptools==65.5.0
+sphinx==4.4
+sphinx_rtd_theme==1.0
+sympy==1.7
+tabulate==0.8.10
+tomli==2.0.1
+tox==3.2.0
+types-all==1.0.0
+typing-extensions==4.2
+xxhash==1.4.4
+##[[[end]]]
diff --git a/min-requirements-test.txt b/min-requirements-test.txt
new file mode 100644
index 00000000..ba296256
--- /dev/null
+++ b/min-requirements-test.txt
@@ -0,0 +1,73 @@
+##[[[cog
+## import re, tomli
+## project = tomli.loads(open("pyproject.toml").read())
+## requirements = set(
+##     rr
+##     for r in (
+##         project["project"]["dependencies"]
+##         + open("requirements-dev.in").readlines()
+##     )
+##     if (rr := r.strip()) and not rr.startswith('#')
+## )
+## for r in sorted(requirements):
+##     m = re.match("^([\w-][\w\d\[\]-]*)[=>~][=]([^,]+)", r)
+##     print(f"{m[1]}=={m[2].strip()}")
+##]]]
+astunparse==1.6.3;python_version<'3.9'
+attrs==21.3
+black==22.3
+boltons==20.1
+cached-property==1.5.1
+clang-format==9.0
+click==8.0.0
+cmake==3.22
+cogapp==3.3
+coverage[toml]==5.0
+cytoolz==0.12.0
+darglint==1.6
+deepdiff==5.6.0
+devtools==0.6
+factory-boy==3.1
+flake8-bugbear==20.11.1
+flake8-builtins==1.5.3
+flake8-debugger==4.0.0
+flake8-docstrings==1.5.0
+flake8-eradicate==1.3.0
+flake8-mutable==1.2.0
+flake8-pyproject==1.2.2
+flake8-rst-docstrings==0.0.14
+flake8==5.0.4
+frozendict==2.3
+gridtools-cpp==2.2.3
+hypothesis==6.0.0
+importlib-resources==5.0;python_version<'3.9'
+isort==5.10
+jinja2==3.0.0
+jupytext==1.14
+lark==1.1.2
+mako==1.1
+mypy==1.0
+ninja==1.10
+numpy==1.21.2
+packaging==20.0
+pip-tools==6.10
+pipdeptree==2.3
+pre-commit==2.17
+psutil==5.0
+pybind11==2.5
+pygments==2.7
+pytest-cache==1.0
+pytest-cov==2.8
+pytest-factoryboy==2.0.3
+pytest-xdist[psutil]==2.4
+pytest==7.0
+setuptools==65.5.0
+sphinx==4.4
+sphinx_rtd_theme==1.0
+tabulate==0.8.10
+tomli==2.0.1
+tox==3.2.0
+types-all==1.0.0
+typing-extensions==4.2
+xxhash==1.4.4
+##[[[end]]]
diff --git a/tests/cartesian_tests/unit_tests/test_gtc/test_npir_codegen.py b/tests/cartesian_tests/unit_tests/test_gtc/test_npir_codegen.py
index 9be83d4b..de3269ff 100644
--- a/tests/cartesian_tests/unit_tests/test_gtc/test_npir_codegen.py
+++ b/tests/cartesian_tests/unit_tests/test_gtc/test_npir_codegen.py
@@ -173,7 +173,7 @@ def test_native_function() -> None:
         is_serial=False,
     )
     print(result)
-    match = re.match(r"np.minimum\(a\[.*\],\s*p\)", result)
+    match = re.match(r"ufuncs.minimum\(a\[.*\],\s*p\)", result)
     assert match
 
 
@@ -299,7 +299,7 @@ def test_computation() -> None:
             r"import numbers\n"
             r"from typing import Tuple\n+"
             r"import numpy as np\n"
-            r"import scipy.special\n+"
+            r"from gt4py.cartesian.gtc import ufuncs\n+"
             r"class Field:\n"
             r"(.*\n)+"
             r"def run\(\*, a, b, _domain_, _origin_\):\n"