diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 216056cf..908f0f45 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -163,6 +163,7 @@ jobs: -DCMAKE_BUILD_TYPE=${{ matrix.config }} \ -DCMAKE_CXX_COMPILER=${{ matrix.cxx }} \ -DCMAKE_C_COMPILER=${{ matrix.cc }} \ + -DXAD_TAPE_REUSE_SLOTS=${{ matrix.reuse_slots }} \ -DCMAKE_INSTALL_PREFIX=${{ github.workspace }}/install - name: build run: | @@ -262,6 +263,7 @@ jobs: -DCMAKE_C_COMPILER=${{ matrix.cc }} \ -DCMAKE_CXX_FLAGS="${{ matrix.coverage_cxx_flags }}" \ -DCMAKE_EXE_LINKER_FLAGS="${{ matrix.coverage_ld_flags }}" \ + -DXAD_TAPE_REUSE_SLOTS=${{ matrix.reuse_slots }} \ -DCMAKE_INSTALL_PREFIX=${{ env.GITHUB_WORKSPACE }}/install - name: build run: | diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml new file mode 100644 index 00000000..8b458265 --- /dev/null +++ b/.github/workflows/wheels.yml @@ -0,0 +1,125 @@ +# builds only on the following conditions: +# - pull requests into main +# - OR pushes of tags starting with v* +# - OR manual dispatch on repo +# publishes to test pypi if: +# - in auto-differentiation/XAD repository AND +# - pushes of tags starting with v* +# - OR manual dispatch on repo +# publishes to real PyPI if: +# - publish to Test PyPI worked (with all build conditions above) +# - and if it's a version tag (starting with v*) +# + +name: Python Wheels +on: + pull_request: + branches: + - main + push: + tags: + - v* + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read # to fetch code (actions/checkout) + +jobs: + build_wheels: + name: Wheels + strategy: + fail-fast: false + matrix: + buildplat: ["manylinux_x86_64", "musllinux_x86_64", "macosx_x86_64", "win_amd64"] + python: ["cp38", "cp39", "cp310", "cp311", "cp312"] + include: + - buildplat: "manylinux_x86_64" + os: ubuntu-20.04 + python_exe: "$(which python)" + - buildplat: "musllinux_x86_64" + os: ubuntu-20.04 + python_exe: "$(which python)" + - buildplat: "macosx_x86_64" + os: "macos-12" + python_exe: "$(which python)" + - buildplat: "win_amd64" + os: windows-2022 + python_exe: "python" + exclude: + # gives "is not a supported wheel on this platform" for some reason + - buildplat: "macosx_x86_64" + python: "cp38" + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - name: Build wheels + uses: pypa/cibuildwheel@v2.16.5 + env: + CIBW_BUILD: ${{ matrix.python }}-${{ matrix.buildplat }} + CIBW_BEFORE_BUILD: pip install poetry && mkdir build && cd build && cmake .. -DCMAKE_BUILD_TYPE=Release -DXAD_ENABLE_TESTS=OFF -DXAD_ENABLE_PYTHON=ON -DPYTHON_EXECUTABLE=${{ matrix.python_exe }} -DXAD_STATIC_MSVC_RUNTIME=ON && cmake --build . --config Release + CIBW_TEST_COMMAND: pytest {package}/tests + CIBW_TEST_REQUIRES: pytest + with: + package-dir: bindings/python + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: cibw-wheels-${{ matrix.python }}-${{ matrix.buildplat }} + path: ./wheelhouse/*.whl + if-no-files-found: error + + test-publish: + needs: build_wheels + if: >- + github.repository == 'auto-differentiation/XAD' && + (github.event_name == 'workflow_dispatch' || + (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v'))) + environment: + name: testpypi + url: https://test.pypi.org/p/xad-autodiff + permissions: + id-token: write + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v4 + with: + pattern: cibw-* + path: dist + merge-multiple: true + - uses: pypa/gh-action-pypi-publish@v1.8.12 + name: Publish on Test PyPI + with: + verbose: true + repository-url: https://test.pypi.org/legacy/ + skip-existing: true + + publish: + runs-on: ubuntu-latest + needs: test-publish + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + environment: + name: pypi + url: https://pypi.org/p/xad-autodiff + permissions: + id-token: write + steps: + - uses: actions/download-artifact@v4 + with: + pattern: cibw-* + path: dist + merge-multiple: true + - uses: pypa/gh-action-pypi-publish@v1.8.12 + name: Publish on PyPI + with: + verbose: true + skip-existing: true + diff --git a/.gitignore b/.gitignore index 2d2a76ad..0f376317 100644 --- a/.gitignore +++ b/.gitignore @@ -395,4 +395,5 @@ Mkfile.old dkms.conf /site -.venv \ No newline at end of file +.venv +CMakeUserPresets.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 48391e03..835ea140 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,15 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Support for enhanced debugger visualisations in Visual Studio (@dholden3) +- Python bindings as [xad-autodiff](https://pypi.org/project/xad-autodiff/) +- Added `std::is_signed` trait to `StdCompatibility.hpp` header for consistency +- Support for enhanced debugger visualisations in Visual Studio (@dholden3) ### Changed -### Deprecated - -### Removed - -### Fixed +- Improved documentation for quantlib-xad build +- Cleaned up output of Swap Pricer example ## [1.4.1] - 2024-01-10 @@ -77,10 +76,13 @@ This is a patch release to ensure compatibility with QuantLib 1.33. ### Added -- QuantLib integration by means of the [quantlib-xad](https://github.com/auto-differentiation/quantlib-xad) integration module +- QuantLib integration by means of the + [quantlib-xad](https://github.com/auto-differentiation/quantlib-xad) + integration module - Full MacOS support - Better CI pipeline with more platforms and compilers tested -- Code coverage and quality measured on pull requests and reported in [README.md](README.md) +- Code coverage and quality measured on pull requests and reported + in [README.md](README.md) - More tests to improve code coverage - Status badges in [README.md](README.md) - Documentation updates diff --git a/CMakeLists.txt b/CMakeLists.txt index 8650e278..d62b9405 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,12 +45,23 @@ include(SetupOptions) if(NOT XAD_DOCS_ONLY) include(SetupCompiler) - add_subdirectory(src) + if(XAD_ENABLE_PYTHON) + include(SetupPython) + endif() if(XAD_ENABLE_TESTS) include(SetupTesting) + endif() + + add_subdirectory(src) + + if(XAD_ENABLE_TESTS) add_subdirectory(test) add_subdirectory(samples) endif() + + if(XAD_ENABLE_PYTHON) + add_subdirectory(bindings/python) + endif() endif() install(FILES CHANGELOG.md LICENSE.md TYPE DOC) diff --git a/bindings/python/.gitignore b/bindings/python/.gitignore new file mode 100644 index 00000000..82c714ae --- /dev/null +++ b/bindings/python/.gitignore @@ -0,0 +1,54 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ +*.pyi +prebuilt_file.txt \ No newline at end of file diff --git a/bindings/python/CMakeLists.txt b/bindings/python/CMakeLists.txt new file mode 100644 index 00000000..abf6714a --- /dev/null +++ b/bindings/python/CMakeLists.txt @@ -0,0 +1,35 @@ +############################################################################## +# +# Python bindings for XAD - poetry-based setup +# +# This file is part of XAD, a comprehensive C++ library for +# automatic differentiation. +# +# Copyright (C) 2010-2024 Xcelerit Computing Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +# build _xad_autodiff binary module +add_subdirectory(src) + +if(XAD_ENABLE_TESTS) + add_subdirectory(tests) + add_subdirectory(samples) +endif() + + + + diff --git a/bindings/python/README.md b/bindings/python/README.md new file mode 100644 index 00000000..be3384be --- /dev/null +++ b/bindings/python/README.md @@ -0,0 +1,90 @@ +[![Python](https://img.shields.io/pypi/pyversions/xad-autodiff.svg)](https://auto-differentiation.github.io/python) +[![PyPI version](https://badge.fury.io/py/xad-autodiff.svg)](https://pypi.org/project/xad-autodiff/) + + +XAD is a library designed for +[automatic differentiation](https://auto-differentiation.github.io/aad/), +aimed at both beginners and advanced users. It is intended for use in +production environments, emphasizing performance and ease of use. The library +facilitates the computation of derivatives within computer programs, making +the process efficient and straightforward for a wide range of mathematical +functions, from simple arithmetic to complex calculations, ensuring accurate +and automatic derivative computations. + +The Python bindings for XAD offer the following features: + +- Support for both forward and adjoint modes at the first order. +- Strong exception-safety guarantees. +- High performance, as demonstrated in extensive production use. + +For more details and to integrate XAD into your projects, consult the +comprehensive [documentation](https://auto-differentiation.github.io/python). + +## Application Areas + +Automatic differentiation has many application areas, for example: + +- **Machine Learning and Deep Learning:** Training neural networks or other + machine learning models. +- **Optimization:** Solving optimization problems in engineering and finance. +- **Numerical Analysis:** Enhancing numerical solution methods for + differential equations. +- **Scientific Computing:** Simulating physical systems and processes. +- **Risk Management and Quantitative Finance:** Assessing and hedging risk in + financial models. +- **Computer Graphics:** Optimizing rendering algorithms. +- **Robotics:** Improving control and simulation of robotic systems. +- **Meteorology:** Enhancing weather prediction models. +- **Biotechnology:** Modeling biological processes and systems. + +## Getting Started + +Install: + +```text +pip install xad-autodiff +``` + + +Calculate first-order derivatives in adjoint mode: + +```python +import xad_autodiff.adj_1st as xadj + + +# set independent variables +x0_ad = xadj.Real(1.0) +x1_ad = xadj.Real(1.5) +x2_ad = xadj.Real(1.3) +x3_ad = xadj.Real(1.2) + +with xadj.Tape() as tape: + # and register them + tape.registerInput(x0_ad) + tape.registerInput(x1_ad) + tape.registerInput(x2_ad) + tape.registerInput(x3_ad) + + # start recording derivatives + tape.newRecording() + + # calculate the output + y = x0_ad + x1_ad - x2_ad * x3_ad + + # register and seed adjoint of output + tape.registerOutput(y) + y.derivative = 1.0 + + # compute all other adjoints + tape.computeAdjoints() + + # output results + print(f"y = {y}") + print(f"first order derivatives:\n") + print(f"dy/dx0 = {x0_ad.derivative}") + print(f"dy/dx1 = {x1_ad.derivative}") + print(f"dy/dx2 = {x2_ad.derivative}") + print(f"dy/dx3 = {x3_ad.derivative}") +``` + +For more information, see the [Documentation](https://auto-differentiation.github.io/python). diff --git a/bindings/python/build.py b/bindings/python/build.py new file mode 100644 index 00000000..3cba4d27 --- /dev/null +++ b/bindings/python/build.py @@ -0,0 +1,96 @@ +############################################################################## +# +# Build file for extension module - using pre-built binary with pybind. +# +# This was inspired by: +# https://github.com/tim-mitchell/prebuilt_binaries/tree/main +# +# This file is part of XAD, a comprehensive C++ library for +# automatic differentiation. +# +# Copyright (C) 2010-2024 Xcelerit Computing Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from distutils.file_util import copy_file +import os +from pathlib import Path + +try: + from setuptools import Extension as _Extension + from setuptools.command.build_ext import build_ext as _build_ext +except ImportError: + from distutils.command.build_ext import ( # type: ignore[assignment] + build_ext as _build_ext, + ) + from distutils.extension import Extension as _Extension # type: ignore[assignment] + + +class XadExtension(_Extension): + """Extension module for XAD, using the pre-built file from CMAKE instead of + actually building it.""" + + def __init__(self, name: str, input_file: str): + filepath = Path(input_file) + if not filepath.exists(): + raise ValueError(f"extension file {input_file} does not exist") + self.input_file = input_file + + super().__init__(f"xad_autodiff.{name}", ["dont-need-this-source-file.c"]) + + +class build_ext(_build_ext): + """Overrides build_ext to simply copy the file built with CMake into the + right location, rather than actually building it""" + + def run(self): + for ext in self.extensions: + if not isinstance(ext, XadExtension): + raise ValueError("Only pre-built extensions supported") + + fullname = self.get_ext_fullname(ext.name) + filename = self.get_ext_filename(fullname) + dest_path = Path(self.build_lib) / "xad_autodiff" + dest_path.mkdir(parents=True, exist_ok=True) + dest_filename = dest_path / os.path.basename(filename) + + copy_file( + ext.input_file, + dest_filename, + verbose=self.verbose, + dry_run=self.dry_run, + ) + + if self.inplace: + self.copy_extensions_to_source() + + +def build(setup_kwargs: dict): + """Main extension build command. Needs to have the file `prebuilt_file.txt` + in the same directory, holding the file name of the dynamic library that has + been prebuilt, so that it can copy it to the right location.""" + + prebuilt_file = Path(__file__).parent / "prebuilt_file.txt" + with open(prebuilt_file, "rt") as f: + filename = f.read().strip() + ext_modules = [XadExtension("_xad_autodiff", filename)] + setup_kwargs.update( + { + "ext_modules": ext_modules, + "cmdclass": {"build_ext": build_ext}, + "zip_safe": False, + } + ) diff --git a/bindings/python/poetry.lock b/bindings/python/poetry.lock new file mode 100644 index 00000000..c0fd7051 --- /dev/null +++ b/bindings/python/poetry.lock @@ -0,0 +1,335 @@ +# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. + +[[package]] +name = "black" +version = "24.2.0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "black-24.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6981eae48b3b33399c8757036c7f5d48a535b962a7c2310d19361edeef64ce29"}, + {file = "black-24.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d533d5e3259720fdbc1b37444491b024003e012c5173f7d06825a77508085430"}, + {file = "black-24.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61a0391772490ddfb8a693c067df1ef5227257e72b0e4108482b8d41b5aee13f"}, + {file = "black-24.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:992e451b04667116680cb88f63449267c13e1ad134f30087dec8527242e9862a"}, + {file = "black-24.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:163baf4ef40e6897a2a9b83890e59141cc8c2a98f2dda5080dc15c00ee1e62cd"}, + {file = "black-24.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e37c99f89929af50ffaf912454b3e3b47fd64109659026b678c091a4cd450fb2"}, + {file = "black-24.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9de21bafcba9683853f6c96c2d515e364aee631b178eaa5145fc1c61a3cc92"}, + {file = "black-24.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:9db528bccb9e8e20c08e716b3b09c6bdd64da0dd129b11e160bf082d4642ac23"}, + {file = "black-24.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d84f29eb3ee44859052073b7636533ec995bd0f64e2fb43aeceefc70090e752b"}, + {file = "black-24.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e08fb9a15c914b81dd734ddd7fb10513016e5ce7e6704bdd5e1251ceee51ac9"}, + {file = "black-24.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:810d445ae6069ce64030c78ff6127cd9cd178a9ac3361435708b907d8a04c693"}, + {file = "black-24.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ba15742a13de85e9b8f3239c8f807723991fbfae24bad92d34a2b12e81904982"}, + {file = "black-24.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7e53a8c630f71db01b28cd9602a1ada68c937cbf2c333e6ed041390d6968faf4"}, + {file = "black-24.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:93601c2deb321b4bad8f95df408e3fb3943d85012dddb6121336b8e24a0d1218"}, + {file = "black-24.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0057f800de6acc4407fe75bb147b0c2b5cbb7c3ed110d3e5999cd01184d53b0"}, + {file = "black-24.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:faf2ee02e6612577ba0181f4347bcbcf591eb122f7841ae5ba233d12c39dcb4d"}, + {file = "black-24.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:057c3dc602eaa6fdc451069bd027a1b2635028b575a6c3acfd63193ced20d9c8"}, + {file = "black-24.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08654d0797e65f2423f850fc8e16a0ce50925f9337fb4a4a176a7aa4026e63f8"}, + {file = "black-24.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca610d29415ee1a30a3f30fab7a8f4144e9d34c89a235d81292a1edb2b55f540"}, + {file = "black-24.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:4dd76e9468d5536abd40ffbc7a247f83b2324f0c050556d9c371c2b9a9a95e31"}, + {file = "black-24.2.0-py3-none-any.whl", hash = "sha256:e8a6ae970537e67830776488bca52000eaa37fa63b9988e8c487458d9cd5ace6"}, + {file = "black-24.2.0.tar.gz", hash = "sha256:bce4f25c27c3435e4dace4815bcb2008b87e167e3bf4ee47ccdc5ce906eb4894"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.0" +description = "Backport of PEP 654 (exception groups)" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, + {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "flake8" +version = "7.0.0" +description = "the modular source code checker: pep8 pyflakes and co" +category = "dev" +optional = false +python-versions = ">=3.8.1" +files = [ + {file = "flake8-7.0.0-py2.py3-none-any.whl", hash = "sha256:a6dfbb75e03252917f2473ea9653f7cd799c3064e54d4c8140044c5c065f53c3"}, + {file = "flake8-7.0.0.tar.gz", hash = "sha256:33f96621059e65eec474169085dc92bf26e7b2d47366b70be2f67ab80dc25132"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.11.0,<2.12.0" +pyflakes = ">=3.2.0,<3.3.0" + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "mypy" +version = "1.8.0" +description = "Optional static typing for Python" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3"}, + {file = "mypy-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4"}, + {file = "mypy-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d"}, + {file = "mypy-1.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9"}, + {file = "mypy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410"}, + {file = "mypy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae"}, + {file = "mypy-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3"}, + {file = "mypy-1.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817"}, + {file = "mypy-1.8.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d"}, + {file = "mypy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835"}, + {file = "mypy-1.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd"}, + {file = "mypy-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55"}, + {file = "mypy-1.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218"}, + {file = "mypy-1.8.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3"}, + {file = "mypy-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e"}, + {file = "mypy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6"}, + {file = "mypy-1.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66"}, + {file = "mypy-1.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6"}, + {file = "mypy-1.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d"}, + {file = "mypy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02"}, + {file = "mypy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8"}, + {file = "mypy-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259"}, + {file = "mypy-1.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b"}, + {file = "mypy-1.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592"}, + {file = "mypy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a"}, + {file = "mypy-1.8.0-py3-none-any.whl", hash = "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d"}, + {file = "mypy-1.8.0.tar.gz", hash = "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.1.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +category = "dev" +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "packaging" +version = "23.2" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "platformdirs" +version = "4.2.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, + {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] + +[[package]] +name = "pluggy" +version = "1.4.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, + {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pybind11-stubgen" +version = "2.5" +description = "PEP 561 type stubs generator for pybind11 modules" +category = "dev" +optional = false +python-versions = "~=3.7" +files = [ + {file = "pybind11-stubgen-2.5.tar.gz", hash = "sha256:96a7febcab248bf98abd4bb72cc5f729ba87b2344247454c1759e63cde9fef34"}, + {file = "pybind11_stubgen-2.5-py3-none-any.whl", hash = "sha256:a2c40ab347d9918e6ab807fd7739fa5cbc19bdb1c69b7cc07fedb1a7e93446a9"}, +] + +[[package]] +name = "pycodestyle" +version = "2.11.1" +description = "Python style guide checker" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, + {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, +] + +[[package]] +name = "pyflakes" +version = "3.2.0" +description = "passive checker of Python programs" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, + {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, +] + +[[package]] +name = "pytest" +version = "8.0.2" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.0.2-py3-none-any.whl", hash = "sha256:edfaaef32ce5172d5466b5127b42e0d6d35ebbe4453f0e3505d96afd93f6b096"}, + {file = "pytest-8.0.2.tar.gz", hash = "sha256:d4051d623a2e0b7e51960ba963193b09ce6daeb9759a451844a21e4ddedfc1bd"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.3.0,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "typing-extensions" +version = "4.10.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, + {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, +] + +[metadata] +lock-version = "2.0" +python-versions = ">=3.8.1,<4.0" +content-hash = "a19c9e040a8765658ec819821bd75f2053624dca57d2c72e54a70fdd4e352693" diff --git a/bindings/python/pyproject.toml b/bindings/python/pyproject.toml new file mode 100644 index 00000000..0eeaa161 --- /dev/null +++ b/bindings/python/pyproject.toml @@ -0,0 +1,106 @@ +############################################################################## +# +# Python bindings for XAD - poetry-based setup file +# +# This file is part of XAD, a comprehensive C++ library for +# automatic differentiation. +# +# Copyright (C) 2010-2024 Xcelerit Computing Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +[tool.poetry] +name = "xad-autodiff" +version = "0.0.0" +description = "High-Performance Automatic Differentiation for Python" +authors = ["Auto Differentiation Dev Team "] +readme = "README.md" +homepage = "https://auto-differentiation.github.io" +repository = "https://github.com/auto-differentiation/XAD" +documentation = "https://auto-differentiation.github.io/python" +keywords = [ + "automatic-differentiation", + "derivatives", + "machine-learning", + "optimisation", + "numerical-analysis", + "scientific-computing", + "risk-management", + "computer-graphics", + "robotics", + "biotechnology", + "meteorology", + "quant-finance" +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: Education", + "Intended Audience :: Financial and Insurance Industry", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)", + "License :: Other/Proprietary License", + "Operating System :: MacOS :: MacOS X", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX :: Linux", + "Natural Language :: English", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: Implementation :: CPython", + "Topic :: Scientific/Engineering", + "Topic :: Software Development" +] +license = "AGPL-3.0-or-later" +include = ["xad_autodiff/_xad_autodiff.*", "xad_autodiff/**/*.pyi"] + +[tool.poetry.urls] +Download = "https://pypi.org/project/xad-autodiff/#files" +Tracker = "https://github.com/auto-differentiation/XAD/issues" +"Release notes" = "https://github.com/auto-differentiation/XAD/releases" + +[tool.poetry.build] +script = "build.py" +generate-setup-file = true + +[tool.poetry-dynamic-versioning] +enable = true +metadata = false + +[tool.poetry.dependencies] +python= ">=3.8.1,<4.0" + +[tool.poetry.group.dev.dependencies] +mypy = "*" +black = "*" +flake8 = "*" +pytest = "*" +pybind11-stubgen = "^2.5" + +[build-system] +requires = [ + "poetry-core>=1.0.0", + "poetry-dynamic-versioning>=1.0.0,<2.0.0", + "setuptools>=42" +] +build-backend = "poetry_dynamic_versioning.backend" + + +[tool.black] +line-length = 100 +target-version = ["py38", "py39", "py310", "py311", "py312"] \ No newline at end of file diff --git a/bindings/python/samples/CMakeLists.txt b/bindings/python/samples/CMakeLists.txt new file mode 100644 index 00000000..2b76e978 --- /dev/null +++ b/bindings/python/samples/CMakeLists.txt @@ -0,0 +1,33 @@ +############################################################################## +# +# Adds samples as tests to the build, to ensure error-free execution +# +# This file is part of XAD, a comprehensive C++ library for +# automatic differentiation. +# +# Copyright (C) 2010-2024 Xcelerit Computing Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +function(add_python_sample_test name ) + add_test(NAME "python_sample_${name}" + COMMAND ${POETRY_EXECUTABLE} run python "${name}.py" + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) +endfunction() + +add_python_sample_test(adj_1st) +add_python_sample_test(fwd_1st) +add_python_sample_test(swap_pricer) \ No newline at end of file diff --git a/bindings/python/samples/adj_1st.py b/bindings/python/samples/adj_1st.py new file mode 100644 index 00000000..089083c2 --- /dev/null +++ b/bindings/python/samples/adj_1st.py @@ -0,0 +1,71 @@ +############################################################################## +# +# Sample for first-order adjoint calculation with Python +# +# Computes +# y = f(x0, x1, x2, x3) +# and its first order derivatives +# dy/dx0, dy/dx1, dy/dx2, dy/dx3 +# using adjoint mode. +# +# This file is part of XAD, a comprehensive C++ library for +# automatic differentiation. +# +# Copyright (C) 2010-2024 Xcelerit Computing Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import xad_autodiff.adj_1st as xadj + + +# input values +x0 = 1.0 +x1 = 1.5 +x2 = 1.3 +x3 = 1.2 + +# set independent variables +x0_ad = xadj.Real(x0) +x1_ad = xadj.Real(x1) +x2_ad = xadj.Real(x2) +x3_ad = xadj.Real(x3) + +with xadj.Tape() as tape: + # and register them + tape.registerInput(x0_ad) + tape.registerInput(x1_ad) + tape.registerInput(x2_ad) + tape.registerInput(x3_ad) + + # start recording derivatives + tape.newRecording() + + # calculate the output + y = x0_ad + x1_ad - x2_ad * x3_ad + + # register and seed adjoint of output + tape.registerOutput(y) + y.derivative = 1.0 + + # compute all other adjoints + tape.computeAdjoints() + + # output results + print(f"y = {y}") + print(f"first order derivatives:\n") + print(f"dy/dx0 = {x0_ad.derivative}") + print(f"dy/dx1 = {x1_ad.derivative}") + print(f"dy/dx2 = {x2_ad.derivative}") + print(f"dy/dx3 = {x3_ad.derivative}") diff --git a/bindings/python/samples/fwd_1st.py b/bindings/python/samples/fwd_1st.py new file mode 100644 index 00000000..42aac847 --- /dev/null +++ b/bindings/python/samples/fwd_1st.py @@ -0,0 +1,56 @@ +############################################################################## +# +# Sample for 1st order forward mode in Python. +# +# Computes +# y = f(x0, x1, x2, x3) +# and it's first order derivative w.r.t. x0 using forward mode: +# dy/dx0 +# +# This file is part of XAD, a comprehensive C++ library for +# automatic differentiation. +# +# Copyright (C) 2010-2024 Xcelerit Computing Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + + +import xad_autodiff.fwd_1st as xfwd + +# input values +x0 = 1.0 +x1 = 1.5 +x2 = 1.3 +x3 = 1.2 + +# set independent variables +x0_ad = xfwd.Real(x0) +x1_ad = xfwd.Real(x1) +x2_ad = xfwd.Real(x2) +x3_ad = xfwd.Real(x3) + +# compute derivative w.r.t. x0 +# (if other derivatives are needed, the initial derivatives have to be reset +# and the function run again) +x0_ad.derivative = 1.0 + +# run the algorithm with active variables +y = 2 * x0_ad + x1_ad - x2_ad * x3_ad + +# output results{ +print(f"y = {y.value}") +print("first order derivative:") +print(f"dy/dx0 = {y.derivative}") diff --git a/bindings/python/samples/swap_pricer.py b/bindings/python/samples/swap_pricer.py new file mode 100644 index 00000000..333e6f12 --- /dev/null +++ b/bindings/python/samples/swap_pricer.py @@ -0,0 +1,91 @@ +############################################################################## +# +# Computes the discount rate sensitivities of a simple swap pricer +# using adjoint mode. +# +# This file is part of XAD, a comprehensive C++ library for +# automatic differentiation. +# +# Copyright (C) 2010-2024 Xcelerit Computing Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from random import randint +from typing import List +from xad_autodiff import math +import xad_autodiff.adj_1st as xadj + + +def calculate_price_swap( + disc_rates: List[xadj.Real], + is_fixed_pay: bool, + mat: List[float], + float_rates: List[float], + fixed_rate: float, + face_value: float, +): + """Calculates the Swap price, given maturities (in years), float and fixed rates + at the given maturities, and the face value""" + + # discounted fixed cashflows + b_fix = sum(face_value * fixed_rate / math.pow(1 + r, T) for r, T in zip(disc_rates, mat)) + # notional exchange at the end + b_fix += face_value / math.pow(1.0 + disc_rates[-1], mat[-1]) + # discounted float cashflows + b_flt = sum( + face_value * f / math.pow(1 + r, T) for f, r, T in zip(float_rates, disc_rates, mat) + ) + # notional exchange at the end + b_flt += face_value / math.pow(1.0 + disc_rates[-1], mat[-1]) + + return b_flt - b_fix if is_fixed_pay else b_fix - b_flt + + +# initialise input data +n_rates = 30 +face_value = 10000000.0 +fixed_rate = 0.03 +is_fixed_pay = True +rand_max = 214 +float_rates = [0.01 + randint(0, rand_max) / rand_max * 0.1 for _ in range(n_rates)] +disc_rates = [0.01 + randint(0, rand_max) / rand_max * 0.06 for _ in range(n_rates)] +maturities = list(range(1, n_rates + 1)) + +disc_rates_d = [xadj.Real(r) for r in disc_rates] + +with xadj.Tape() as tape: + # set independent variables + tape.registerInputs(disc_rates_d) + + # start recording derivatives + tape.newRecording() + + v = calculate_price_swap( + disc_rates_d, is_fixed_pay, maturities, float_rates, fixed_rate, face_value + ) + + # seed adjoint of output + tape.registerOutput(v) + v.derivative = 1.0 + + # compute all other adjoints + tape.computeAdjoints() + + # output results + print(f"v = {v.value:.2f}") + print("Discount rate sensitivities for 1 basispoint shift:") + for i, rate in enumerate(disc_rates_d): + print(f"dv/dr{i} = {rate.derivative * 0.0001:.2f}") diff --git a/bindings/python/src/CMakeLists.txt b/bindings/python/src/CMakeLists.txt new file mode 100644 index 00000000..bade4a3a --- /dev/null +++ b/bindings/python/src/CMakeLists.txt @@ -0,0 +1,74 @@ +############################################################################## +# +# Python bindings using Pybind11 - exension module. +# +# This file is part of XAD, a comprehensive C++ library for +# automatic differentiation. +# +# Copyright (C) 2010-2024 Xcelerit Computing Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +pybind11_add_module(_xad_autodiff MODULE + module.cpp math.hpp tape.hpp real.hpp exceptions.hpp + ${PROJECT_SOURCE_DIR}/src/Tape.cpp +) + +add_dependencies(_xad_autodiff xad) +target_include_directories(_xad_autodiff PRIVATE ${PROJECT_BINARY_DIR}/src ${PROJECT_SOURCE_DIR}/src) +target_compile_options(_xad_autodiff PRIVATE ${xad_cxx_flags} ${xad_cxx_extra}) + +set_target_properties(_xad_autodiff PROPERTIES CXX_STANDARD 17) +if(MSVC) + # to allow using M_PI and the like + target_compile_definitions(_xad_autodiff PRIVATE _USE_MATH_DEFINES) + # respect the static build setting - we do static builds on windows to avoid dependency problems + # with Python + if(XAD_STATIC_MSVC_RUNTIME) + set_target_properties(_xad_autodiff PROPERTIES + MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") + else() + set_target_properties(_xad_autodiff PROPERTIES + MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") + endif() +endif() + +# write filename into source dir, so that python build can find it +add_custom_command(TARGET _xad_autodiff POST_BUILD + COMMAND echo $ > ${CMAKE_CURRENT_SOURCE_DIR}/../prebuilt_file.txt) + +# setup venv using poetry + build with wheel, producing a stamp value to hook dependencies +add_custom_command( + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/wheel.stamp + DEPENDS _xad_autodiff + ${CMAKE_CURRENT_SOURCE_DIR}/../pyproject.toml + ${CMAKE_CURRENT_SOURCE_DIR}/../poetry.lock + # install environment + COMMAND ${POETRY_EXECUTABLE} install + # generate stubs for typings + COMMAND ${POETRY_EXECUTABLE} run pybind11-stubgen xad_autodiff --output-dir . + # build the Python weel for the package + COMMAND ${POETRY_EXECUTABLE} build -f wheel + # update the stamp file, to keep track of when we last build it + COMMAND ${CMAKE_COMMAND} -E touch ${CMAKE_CURRENT_BINARY_DIR}/wheel.stamp + COMMENT "Building Python wheel..." + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/.. +) + +# main target for building the python wheel - custom command above will be hooked to this target +add_custom_target(python_wheel ALL + DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/wheel.stamp) + diff --git a/bindings/python/src/exceptions.hpp b/bindings/python/src/exceptions.hpp new file mode 100644 index 00000000..1145c189 --- /dev/null +++ b/bindings/python/src/exceptions.hpp @@ -0,0 +1,53 @@ +/******************************************************************************* + + Exports all XAD exceptions to Python. + + This file is part of XAD, a comprehensive C++ library for + automatic differentiation. + + Copyright (C) 2010-2024 Xcelerit Computing Ltd. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +******************************************************************************/ + +#pragma once + +#include +#include +#include + +namespace py = pybind11; + +void py_exceptions(py::module_& m) +{ + + py::module_ exceptions = m.def_submodule("exceptions"); + auto& xad_exception = py::register_exception(exceptions, "XadException"); + xad_exception.doc() = "Base class for all exceptions raised by XAD"; + py::register_exception(exceptions, "TapeAlreadyActive", xad_exception) + .doc() = + "Raised when activating a tape when this or another tape is already active in the current " + "thread"; + py::register_exception(exceptions, "OutOfRange", xad_exception).doc() = + "raised when setting a derivative at a slot that is out of range of the recorded variables"; + py::register_exception(exceptions, "DerivativesNotInitialized", + xad_exception) + .doc() = + "Raised when setting derivatives on the tape without a recording and registered outputs"; + py::register_exception(exceptions, "NoTapeException", xad_exception) + .doc() = + "raised if an opteration that requires an active tape is performed while not tape is " + "active"; +} diff --git a/bindings/python/src/math.hpp b/bindings/python/src/math.hpp new file mode 100644 index 00000000..aab4786e --- /dev/null +++ b/bindings/python/src/math.hpp @@ -0,0 +1,234 @@ +/******************************************************************************* + + Exports all XAD math functions to Python. + + This file is part of XAD, a comprehensive C++ library for + automatic differentiation. + + Copyright (C) 2010-2024 Xcelerit Computing Ltd. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +******************************************************************************/ + +#pragma once + +#include +#include +#include +#include + +namespace py = pybind11; + +using AReal = xad::AReal; +using FReal = xad::FReal; + +template +void add_math_functions(py::module_& m) +{ + m.def( + "sqrt", [](const T& d) { return T(xad::sqrt(d)); }, "square root"); + m.def( + "pow", [](const T& d, const double& b) { return T(xad::pow(d, b)); }, "power"); + m.def( + "pow", [](const double& d, const T& b) { return T(xad::pow(d, b)); }, "power"); + m.def( + "pow", [](const T& d, const T& b) { return T(xad::pow(d, b)); }, "power"); + m.def( + "log10", [](const T& d) { return T(xad::log10(d)); }, "base 10 logarithm"); + m.def( + "log", [](const T& d) { return T(xad::log(d)); }, "natural logarithm"); + m.def( + "ldexp", [](const T& d, const int b) { return T(xad::ldexp(d, b)); }, + "mutiplies x by 2 to the power of exp"); + m.def( + "exp", [](const T& d) { return T(xad::exp(d)); }, "exponential function"); + m.def( + "exp2", [](const T& d) { return T(xad::exp2(d)); }, + "computes 2 to the power of the argument"); + m.def( + "expm1", [](const T& d) { return T(xad::expm1(d)); }, "computes exp(x)-1"); + m.def( + "log1p", [](const T& d) { return T(xad::log1p(d)); }, "computes log(1 + x)"); + m.def( + "log2", [](const T& d) { return T(xad::log2(d)); }, "base 2 logarithm"); + + m.def( + "modf", + [](const T& d) + { + double value = 1.0; + T r = xad::modf(d, &value); + return py::make_tuple(r, value); + }, + "decomposes into integral and fractional parts"); + m.def( + "ceil", [](const T& d) { return T(xad::ceil(d)); }, "rounding away from zero"); + m.def( + "floor", [](const T& d) { return T(xad::floor(d)); }, "rounding towards zero"); + m.def( + "frexp", + [](const T& d) + { + int value = 1; + T r = xad::frexp(d, &value); + return py::make_tuple(r, value); + }, + "decomposes into normalised fraction and an integral power of 2"); + m.def( + "fmod", [](const T& d, const T& b) { return T(xad::fmod(d, b)); }, + "floating point remainer after integer division"); + + m.def( + "min", [](const T& d, const T& b) { return T(xad::min(d, b)); }, "minimum of 2 values"); + m.def( + "min", [](const T& d, const double& b) { return T(xad::min(d, b)); }, + "minimum of 2 values"); + m.def( + "min", [](const double& d, const T& b) { return T(xad::min(d, b)); }, + "minimum of 2 values"); + m.def( + "max", [](const T& d, const T& b) { return T(xad::max(d, b)); }, "maximum of 2 values"); + m.def( + "max", [](const T& d, const double& b) { return T(xad::max(d, b)); }, + "maximum of 2 values"); + m.def( + "max", [](const double& d, const T& b) { return T(xad::max(d, b)); }, + "maximum of 2 values"); + m.def( + "fmax", [](const T& d, const T& b) { return T(xad::fmax(d, b)); }, "maximum of 2 values"); + m.def( + "fmax", [](const T& d, const double& b) { return T(xad::fmax(d, b)); }, + "maximum of 2 values"); + m.def( + "fmax", [](const double& d, const T& b) { return T(xad::fmax(d, b)); }, + "maximum of 2 values"); + m.def( + "fmin", [](const T& d, const T& b) { return T(xad::fmin(d, b)); }, "minimum of 2 values"); + m.def( + "fmin", [](const T& d, const double& b) { return T(xad::fmin(d, b)); }, + "minimum of 2 values"); + m.def( + "fmin", [](const double& d, const T& b) { return T(xad::fmin(d, b)); }, + "minimum of 2 values"); + m.def( + "abs", [](const T& d) { return T(xad::abs((d))); }, "absolute value"); + m.def( + "fabs", [](const T& d) { return T(xad::fabs(d)); }, "absolute value"); + + m.def( + "smooth_abs", [](const T& d) { return T(xad::smooth_abs(d)); }, + "smoothed abs function for well-defined derivatives"); + m.def( + "smooth_max", [](const T& d, const T& b) { return T(xad::smooth_max(d, b)); }, + "smoothed max function for well-defined derivatives"); + m.def( + "smooth_max", [](const T& d, const double& b) { return T(xad::smooth_max(d, b)); }, + "smoothed max function for well-defined derivatives"); + m.def( + "smooth_max", [](const double& d, const T& b) { return T(xad::smooth_max(d, b)); }, + "smoothed max function for well-defined derivatives"); + m.def( + "smooth_min", [](const T& d, const T& b) { return T(xad::smooth_min(d, b)); }, + "smoothed min function for well-defined derivatives"); + m.def( + "smooth_min", [](const T& d, const double& b) { return T(xad::smooth_min(d, b)); }, + "smoothed min function for well-defined derivatives"); + m.def( + "smooth_min", [](const double& d, const T& b) { return T(xad::smooth_min(d, b)); }, + "smoothed min function for well-defined derivatives"); + + m.def( + "tan", [](const T& d) { return T(xad::tan(d)); }, "tangent"); + m.def( + "atan", [](const T& d) { return T(xad::atan(d)); }, "inverse tangent"); + m.def( + "tanh", [](const T& d) { return T(xad::tanh(d)); }, "tangent hyperbolicus"); + m.def( + "atan2", [](const T& d, const T& b) { return T(xad::atan2(d, b)); }, + "4-quadrant inverse tangent"); + m.def( + "atan2", [](const T& d, const double& b) { return T(xad::atan2(d, b)); }, + "4 quadrant inverse tangent"); + m.def( + "atan2", [](const double& d, const T& b) { return T(xad::atan2(d, b)); }, + "4 quadrant inverse tangent"); + m.def( + "atanh", [](const T& d) { return T(xad::atanh(d)); }, "inverse tangent hyperbolicus"); + m.def( + "cos", [](const T& d) { return T(xad::cos(d)); }, "cosine"); + m.def( + "acos", [](const T& d) { return T(xad::acos(d)); }, "inverse cosine"); + m.def( + "cosh", [](const T& d) { return T(xad::cosh(d)); }, "cosine hyperbolicus"); + m.def( + "acosh", [](const T& d) { return T(xad::acosh(d)); }, "inverse cosine hyperbolicus"); + m.def( + "sin", [](const T& d) { return T(xad::sin(d)); }, "sine"); + m.def( + "asin", [](const T& d) { return T(xad::asin(d)); }, "inverse sine"); + m.def( + "sinh", [](const T& d) { return T(xad::sinh(d)); }, "sine hyperbolicus"); + m.def( + "asinh", [](const T& d) { return T(xad::asinh(d)); }, "inverse sine hyperbolicus"); + + m.def( + "cbrt", [](const T& d) { return T(xad::cbrt(d)); }, "cubic root"); + m.def( + "erf", [](const T& d) { return T(xad::erf(d)); }, "error function"); + m.def( + "erfc", [](const T& d) { return T(xad::erfc(d)); }, "complementary error function"); + m.def( + "nextafter", [](const T& d, const T& b) { return T(xad::nextafter(d, b)); }, + "next representable value in the given direction"); + m.def( + "nextafter", [](const T& d, const double& b) { return T(xad::nextafter(d, b)); }, + "next representable value in the given direction"); + m.def( + "nextafter", [](const double& d, const T& b) { return T(xad::nextafter(d, b)); }, + "next representable value in the given direction"); + m.def( + "remainder", [](const T& d, const T& b) { return T(xad::remainder(d, b)); }, + "signed remainder after integer division"); + m.def( + "remainder", [](const T& d, const double& b) { return T(xad::remainder(d, b)); }, + "signed remainder after integer division"); + m.def( + "remainder", [](const double& d, const T& b) { return T(xad::remainder(d, b)); }, + "signed remainder after integer division"); + m.def( + "degrees", [](const T& d) { return T((d * 180) / M_PI); }, "convert radians to degrees"); + m.def( + "radians", [](const T& d) { return T((d * M_PI) / 180); }, "convert degrees to radians"); + m.def( + "copysign", [](const double& d, const T& b) { return T(xad::abs(d) * (b / xad::abs(b))); }, + "copy sign of one value to another"); + m.def( + "copysign", [](const T& d, const double& b) { return T(xad::abs(d) * (b / xad::abs(b))); }, + "copy sign of one value to another"); + m.def( + "copysign", [](const T& d, const T& b) { return T(xad::abs(d) * (b / xad::abs(b))); }, + "copy sign of one value to another"); + m.def( + "trunc", [](const T& d) { return T(xad::trunc(d)); }, "cut off decimals"); +}; + +void py_math(py::module& m) +{ + py::module_ m1 = m.def_submodule("math"); + + add_math_functions(m1); + add_math_functions(m1); + add_math_functions(m1); +}; diff --git a/bindings/python/src/module.cpp b/bindings/python/src/module.cpp new file mode 100644 index 00000000..035b74b8 --- /dev/null +++ b/bindings/python/src/module.cpp @@ -0,0 +1,53 @@ +/******************************************************************************* + + Main pybind module definition for the extension module. + + This file is part of XAD, a comprehensive C++ library for + automatic differentiation. + + Copyright (C) 2010-2024 Xcelerit Computing Ltd. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +******************************************************************************/ + +#include +#include +#include "exceptions.hpp" +#include "math.hpp" +#include "real.hpp" +#include "tape.hpp" + +namespace py = pybind11; + +void py_adj_1st(py::module_ &m) +{ + py::module_ adj = m.def_submodule("adj_1st"); + py_real(adj); + py_tape(adj); +} + +void py_fwd_1st(py::module_ &m) +{ + py::module_ fwd = m.def_submodule("fwd_1st"); + py_real(fwd); +} + +PYBIND11_MODULE(_xad_autodiff, m) +{ + py_adj_1st(m); + py_fwd_1st(m); + py_math(m); + py_exceptions(m); +} \ No newline at end of file diff --git a/bindings/python/src/real.hpp b/bindings/python/src/real.hpp new file mode 100644 index 00000000..72a0dc30 --- /dev/null +++ b/bindings/python/src/real.hpp @@ -0,0 +1,196 @@ +/******************************************************************************* + + Defines the bindings for the XAD active types. + + This file is part of XAD, a comprehensive C++ library for + automatic differentiation. + + Copyright (C) 2010-2024 Xcelerit Computing Ltd. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +******************************************************************************/ + +#pragma once + +#include +#include +#include +#include + +namespace py = pybind11; + +using AReal = xad::AReal; +using FReal = xad::FReal; + +inline void add_extra_methods(py::class_& c) +{ + c.def( + "setAdjoint", [](AReal& self, double x) { self.setAdjoint(x); }, + "set adjoint of this variable") + .def( + "shouldRecord", [](const AReal& self) { return self.shouldRecord(); }, + "Check if the variable is registered on tape and should record") + .def( + "getSlot", [](const AReal& self) { return self.getSlot(); }, + "Get the slot of this variable on the tape"); +} + +inline void add_extra_methods(py::class_&) {} + +template +inline T py_fmod(const T1& x, const T2& y) +{ + auto res = T(xad::fmod(x, y)); + if ((res < 0 && y > 0) || (res > 0 && y < 0)) + { + return res + y; + } + return res; +} + +template +inline std::pair py_divmod(const T1& x, const T2& y) +{ + T mod = py_fmod(x, y); + T div = (x - mod) / y; + return {div, mod}; +} + +template +inline T py_floordiv(const T1& x, const T2& y) +{ + return xad::floor(x / y); +} + +template +void py_real(py::module_& m) +{ + auto c = py::class_(m, "Real", py::dynamic_attr(), + "active arithmetic type for first order adjoint mode"); + + c.def(py::init()) + .def(py::init<>()) + .def(py::self == py::self) + .def(py::self != py::self) + .def(py::self >= py::self) + .def(py::self <= py::self) + .def(py::self > py::self) + .def(py::self < py::self) + .def("__int__", [](const T& d) { return int(d.getValue()); }) + .def("__bool__", [](const T& d) { return bool(d); }) + .def("__neg__", [](const T& d) { return T(-d); }) + .def("__pos__", [](const T& d) { return d; }) + .def( + "__add__", [](const T& a, double b) { return T(a + b); }, py::is_operator()) + .def( + "__add__", [](const T& a, const T& b) { return T(a + b); }, py::is_operator()) + .def( + "__radd__", [](const T& a, double b) { return T(a + b); }, py::is_operator()) + .def( + "__mul__", [](const T& a, double b) { return T(a * b); }, py::is_operator()) + .def( + "__mul__", [](const T& a, const T& b) { return T(a * b); }, py::is_operator()) + .def( + "__rmul__", [](const T& a, double b) { return T(a * b); }, py::is_operator()) + .def( + "__sub__", [](const T& a, double b) { return T(a - b); }, py::is_operator()) + .def( + "__sub__", [](const T& a, const T& b) { return T(a - b); }, py::is_operator()) + .def( + "__rsub__", [](const T& a, double b) { return T(b - a); }, py::is_operator()) + .def( + "__truediv__", [](const T& a, double b) { return T(a / b); }, py::is_operator()) + .def( + "__truediv__", [](const T& a, const T& b) { return T(a / b); }, py::is_operator()) + .def( + "__rtruediv__", [](const T& a, double b) { return T(b / a); }, py::is_operator()) + .def("__repr__", [](const T& a) { return std::to_string(a.getValue()); }) + .def( + "__rgt__", [](const T& a, double b) { return (b > a); }, py::is_operator()) + .def( + "__gt__", [](const T& a, double b) { return (a > b); }, py::is_operator()) + .def( + "__rlt__", [](const T& a, double b) { return (b < a); }, py::is_operator()) + .def( + "__lt__", [](const T& a, double b) { return (a < b); }, py::is_operator()) + .def( + "__rge__", [](const T& a, double b) { return (b >= a); }, py::is_operator()) + .def( + "__ge__", [](const T& a, double b) { return (a >= b); }, py::is_operator()) + .def( + "__rle__", [](const T& a, double b) { return (b <= a); }, py::is_operator()) + .def( + "__le__", [](const T& a, double b) { return (a <= b); }, py::is_operator()) + .def( + "__req__", [](const T& a, double b) { return (b == a); }, py::is_operator()) + .def( + "__eq__", [](const T& a, double b) { return (a == b); }, py::is_operator()) + .def( + "__rne__", [](const T& a, double b) { return (b != a); }, py::is_operator()) + .def( + "__ne__", [](const T& a, double b) { return (a != b); }, py::is_operator()) + .def("__round__", + [](const T& x, int ndigits) + { + double factor = std::pow(10, ndigits); + return T(xad::round(x * factor) / factor); + }) + .def("__round__", [](const T& x) { return int(xad::round(x)); }) + .def("__ceil__", [](const T& x) { return int(xad::ceil(x)); }) + .def("__floor__", [](const T& x) { return int(xad::floor(x)); }) + .def("__trunc__", [](const T& x) { return int(xad::trunc(x)); }) + .def("__abs__", [](const T& x) { return T(xad::abs(x)); }) + .def("__pow__", [](const T& x, const T& y) { return T(xad::pow(x, y)); }) + .def("__pow__", [](const T& x, int y) { return T(xad::pow(x, y)); }) + .def("__pow__", [](const T& x, double y) { return T(xad::pow(x, y)); }) + .def("__rpow__", [](const T& x, const T& y) { return T(xad::pow(y, x)); }) + .def("__rpow__", [](const T& x, int y) { return T(xad::pow(y, x)); }) + .def("__rpow__", [](const T& x, double y) { return T(xad::pow(y, x)); }) + .def("__mod__", [](const T& x, const T& y) { return py_fmod(x, y); }) + .def("__mod__", [](const T& x, int y) { return py_fmod(x, y); }) + .def("__mod__", [](const T& x, double y) { return py_fmod(x, y); }) + .def("__rmod__", [](const T& y, const T& x) { return py_fmod(x, y); }) + .def("__rmod__", [](const T& y, int x) { return py_fmod(x, y); }) + .def("__rmod__", [](const T& y, double x) { return py_fmod(x, y); }) + .def("__divmod__", [](const T& x, const T& y) { return py_divmod(x, y); }) + .def("__divmod__", [](const T& x, double y) { return py_divmod(x, y); }) + .def("__divmod__", [](const T& x, int y) { return py_divmod(x, y); }) + .def("__rdivmod__", [](const T& y, const T& x) { return py_divmod(x, y); }) + .def("__rdivmod__", [](const T& y, double x) { return py_divmod(x, y); }) + .def("__rdivmod__", [](const T& y, int x) { return py_divmod(x, y); }) + .def("__floordiv__", [](const T& x, const T& y) { return py_floordiv(x, y); }) + .def("__floordiv__", [](const T& x, double y) { return py_floordiv(x, y); }) + .def("__floordiv__", [](const T& x, int y) { return py_floordiv(x, y); }) + .def("__rfloordiv__", [](const T& y, const T& x) { return py_floordiv(x, y); }) + .def("__rfloordiv__", [](const T& y, double x) { return py_floordiv(x, y); }) + .def("__rfloordiv__", [](const T& y, int x) { return py_floordiv(x, y); }) + // to set/get derivatives + .def( + "getValue", [](const T& self) { return self.getValue(); }, "get the underlying value") + .def( + "setDerivative", [](T& self, double v) { self.setDerivative(v); }, + "set the adjoint of this variable") + .def( + "getDerivative", [](const T& self) { return self.getDerivative(); }, + "get the adjoint of this variable") + .def( + "conjugate", [](const T& x) { return x; }, "complex conjugate") + .def( + "real", [](const T& x) { return x; }, "real part") + .def( + "imag", [](const T&) { return T(0.0); }, "imaginary part"); + + add_extra_methods(c); +} diff --git a/bindings/python/src/tape.hpp b/bindings/python/src/tape.hpp new file mode 100644 index 00000000..a8e34def --- /dev/null +++ b/bindings/python/src/tape.hpp @@ -0,0 +1,108 @@ +/******************************************************************************* + + Defines the bindings for the XAD tape. + + This file is part of XAD, a comprehensive C++ library for + automatic differentiation. + + Copyright (C) 2010-2024 Xcelerit Computing Ltd. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +******************************************************************************/ + +#pragma once + +#include +#include +#include +#include +#include + +namespace py = pybind11; + +using Tape = xad::Tape; +using mode = xad::adj; +using AReal = mode::active_type; + +void py_tape(py::module_ &m) +{ + py::class_(m, "Tape", py::dynamic_attr()) + .def(py::init([] { return std::make_unique(false); }), + "constructs a tape without activating it") + .def( + "__enter__", + [](Tape &self) -> Tape & + { + self.activate(); + return self; + }, + "enters a context `with tape`, activating the tape") + .def( + "__exit__", + [](Tape &self, const std::optional &, + const std::optional &, const std::optional &) + { self.deactivate(); }, + "deactivates the tape when exiting the context") + .def("activate", &Tape::activate, "activate the tape") + .def("deactivate", &Tape::deactivate, "deactivate the tape") + .def("isActive", &Tape::isActive, "check if the tape is active") + .def("getActive", &Tape::getActive, + "class method to get a reference to the currently active tape") + .def("getPosition", &Tape::getPosition, + "get the current position on the tape. Used in conjunction with `computeAdjointsTo`.") + .def("registerInput", py::overload_cast(&Tape::registerInput), + "registers an input variable with tape, for recording") + .def("registerOutput", py::overload_cast(&Tape::registerOutput), + "registers an output with the tape (to be called before setting output adjoints)") + .def("computeAdjoints", &Tape::computeAdjoints, + "Roll back the tape until the point of calling `newRecording`, propagating adjoints " + "from outputs to inputs") + .def("computeAdjointsTo", &Tape::computeAdjointsTo, + "Roll back the tape until the given position (see `getPosition`), propagating " + "adjoints from outputs backwards.") + .def("newRecording", &Tape::newRecording, + "Start a new recording on tape, marking the start of a function to be derived") + .def("clearAll", &Tape::clearAll, + "clear/reset the tape completely, without de-allocating memory. Should be used for " + "re-using the tape, rather than creating a new one") + .def("getMemory", &Tape::getMemory, "Get the total memory consumed by the tape in bytes") + .def("clearDerivatives", &Tape::clearDerivatives, + "clear all derivatives stored on the tape") + .def("clearDerivativesAfter", &Tape::clearDerivativesAfter, + "clear all derivatives after the given position") + .def("resetTo", &Tape::resetTo, "reset the tape back to the given position") + .def("printStatus", &Tape::printStatus, + "output the status of the tape (for debugging/information)") + .def( + "derivative", [](Tape &self, AReal &d) { return self.derivative(d.getSlot()); }, + "get the slot of the given variable") + .def( + "derivative", [](Tape &self, Tape::slot_type slot) { return self.derivative(slot); }, + "get the derivative stored at the given slot position") + .def( + "getDerivative", [](Tape &self, AReal &d) { return self.derivative(d.getSlot()); }, + "alias for `derivative`") + .def( + "getDerivative", [](Tape &self, Tape::slot_type slot) { return self.derivative(slot); }, + "alias for `derivative`") + .def( + "setDerivative", + [](Tape &self, AReal &d, double &b) { return self.setDerivative(d.getSlot(), b); }, + "sets the derivative of the given active variable to the value given") + .def( + "setDerivative", + [](Tape &self, Tape::slot_type slot, double &b) { return self.setDerivative(slot, b); }, + "sets the derivative at the given slot to the given value"); +} diff --git a/bindings/python/tests/CMakeLists.txt b/bindings/python/tests/CMakeLists.txt new file mode 100644 index 00000000..b426c391 --- /dev/null +++ b/bindings/python/tests/CMakeLists.txt @@ -0,0 +1,35 @@ +############################################################################## +# +# Adds Pytest modules for testing the XAD bindings +# +# This file is part of XAD, a comprehensive C++ library for +# automatic differentiation. +# +# Copyright (C) 2010-2024 Xcelerit Computing Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +function(add_python_test name ) + add_test(NAME "python_${name}" + COMMAND ${POETRY_EXECUTABLE} run pytest "test_${name}.py" + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) +endfunction() + + +add_python_test(exceptions) +add_python_test(math_functions_derivatives) +add_python_test(real_operations) +add_python_test(tape) diff --git a/bindings/python/tests/__init__.py b/bindings/python/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bindings/python/tests/test_exceptions.py b/bindings/python/tests/test_exceptions.py new file mode 100644 index 00000000..4134470f --- /dev/null +++ b/bindings/python/tests/test_exceptions.py @@ -0,0 +1,74 @@ +############################################################################## +# +# Test exceptions bindings. +# +# This file is part of XAD, a comprehensive C++ library for +# automatic differentiation. +# +# Copyright (C) 2010-2024 Xcelerit Computing Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +import pytest +from xad_autodiff.adj_1st import Real, Tape +from xad_autodiff.exceptions import ( + XadException, + TapeAlreadyActive, + OutOfRange, + DerivativesNotInitialized, + NoTapeException, +) + + +@pytest.mark.parametrize("exception", [TapeAlreadyActive, XadException]) +def test_exceptions_tape_active(exception): + with Tape() as t: + with pytest.raises(exception) as e: + # when it's already active + t.activate() + assert "A tape is already active for the current thread" in str(e) + + +@pytest.mark.parametrize("exception", [OutOfRange, XadException]) +def test_exceptions_outofrange(exception): + with Tape() as t: + x = Real(1.0) + t.registerInput(x) + assert t.derivative(x) == 0.0 + with pytest.raises(exception) as e: + t.derivative(12312) + assert "given derivative slot is out of range - did you register the outputs?" in str(e) + + +@pytest.mark.parametrize("exception", [DerivativesNotInitialized, XadException]) +def test_exceptions_adjoints_not_initialized(exception): + with Tape() as t: + with pytest.raises(exception) as e: + x = Real(1.0) + t.registerInput(x) + t.newRecording() + y = x * x + t.registerOutput(y) + t.computeAdjoints() + assert "At least one derivative must be set before computing adjoint" in str(e) + + +@pytest.mark.parametrize("exception", [NoTapeException, XadException]) +def test_exceptions_no_tape_exception(exception): + with pytest.raises(exception) as e: + x = Real(1.0) + x.setDerivative(1.0) + assert "No active tape for the current thread" in str(e) diff --git a/bindings/python/tests/test_math_functions_derivatives.py b/bindings/python/tests/test_math_functions_derivatives.py new file mode 100644 index 00000000..70970976 --- /dev/null +++ b/bindings/python/tests/test_math_functions_derivatives.py @@ -0,0 +1,502 @@ +############################################################################## +# +# Pytests for math functions and their derivatives +# +# This file is part of XAD, a comprehensive C++ library for +# automatic differentiation. +# +# Copyright (C) 2010-2024 Xcelerit Computing Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +import sys +import pytest +from xad_autodiff.adj_1st import Tape, Real as Areal +from xad_autodiff.fwd_1st import Real as Freal +from xad_autodiff import math as ad_math +import math + +# This is a list of math functions with their expected outcomes and derivatives, +# used in parametrised tests, for unary functions. +# +# The format is a list of tuples, where each tuple has the following entries: +# - XAD math function: Callable +# - parameter value for the function: float +# - expected result: float +# - expected derivative value: float +# +PARAMETERS_FOR_UNARY_FUNC = [ + (ad_math.sin, math.pi / 4, math.sin(math.pi / 4), math.cos(math.pi / 4)), + (ad_math.cos, math.pi / 4, math.cos(math.pi / 4), -1 * math.sin(math.pi / 4)), + (ad_math.tan, 0.5, math.tan(0.5), 2 / (1 + math.cos(2 * 0.5))), + (ad_math.atan, 0.5, math.atan(0.5), 1 / (1 + math.pow(0.5, 2))), + (ad_math.acos, 0.5, math.acos(0.5), -1 / math.sqrt(1 - math.pow(0.5, 2))), + (ad_math.asin, 0.5, math.asin(0.5), 1 / math.sqrt(1 - math.pow(0.5, 2))), + (ad_math.tanh, 0.5, math.tanh(0.5), 1 - math.pow(math.tanh(0.5), 2)), + (ad_math.cosh, 0.5, math.cosh(0.5), math.sinh(0.5)), + (ad_math.sinh, 0.5, math.sinh(0.5), math.cosh(0.5)), + (ad_math.atanh, 0.5, math.atanh(0.5), 1 / (1 - math.pow(0.5, 2))), + (ad_math.asinh, 0.5, math.asinh(0.5), 1 / math.sqrt(1 + math.pow(0.5, 2))), + (ad_math.acosh, 1.5, math.acosh(1.5), 1 / math.sqrt(math.pow(1.5, 2) - 1)), + (ad_math.sqrt, 4, math.sqrt(4), 1 / (2 * math.sqrt(4))), + (ad_math.log10, 4, math.log10(4), 1 / (4 * math.log(10))), + (ad_math.log, 4, math.log(4), 1 / 4), + (ad_math.exp, 4, math.exp(4), math.exp(4)), + (ad_math.expm1, 4, math.expm1(4), math.exp(4)), + (ad_math.log1p, 4, math.log1p(4), 1 / (5)), + (ad_math.log2, 4, math.log2(4), 1 / (4 * math.log(2))), + (ad_math.abs, -4, abs(-4), -1), + (ad_math.fabs, -4.4, 4.4, -1), + (ad_math.smooth_abs, -4.4, 4.4, -1), + ( + ad_math.erf, + -1.4, + math.erf(-1.4), + (2 / math.sqrt(math.pi)) * math.exp(-1 * math.pow(-1.4, 2)), + ), + ( + ad_math.erfc, + -1.4, + math.erfc(-1.4), + (-2 / math.sqrt(math.pi)) * math.exp(-1 * math.pow(-1.4, 2)), + ), + (ad_math.cbrt, 8, 2.0, (1 / 3) * (math.pow(8, (-2 / 3)))), + (ad_math.trunc, 8.1, math.trunc(8.1), 0), + (ad_math.ceil, 3.7, math.ceil(3.7), 0), + (ad_math.floor, 3.7, math.floor(3.7), 0), +] + + +# This is a list of math functions with their expected outcomes and derivatives, +# used in parametrised tests, for binary functions. +# +# The format is a list of tuples, where each tuple has the following entries: +# - XAD math function: Callable +# - parameter1 value for the function: float +# - parameter2 value for the function: float +# - expected result: float +# - expected derivative1 value: float +# - expected derivative2 value: float +# +PARAMETERS_FOR_BINARY_FUNC = [ + (ad_math.min, 3, 4, 3, 1, 0), + (ad_math.min, 4, 3, 3, 0, 1), + (ad_math.max, 3, 4, 4, 0, 1), + (ad_math.max, 4, 3, 4, 1, 0), + (ad_math.fmin, 3.5, 4.3, 3.5, 1, 0), + (ad_math.fmin, 4.3, 3.5, 3.5, 0, 1), + (ad_math.fmax, 3.5, 4.3, 4.3, 0, 1), + (ad_math.fmax, 4.3, 3.5, 4.3, 1, 0), + (ad_math.smooth_min, 3.5, 4.3, 3.5, 1, 0), + (ad_math.smooth_min, 4.3, 3.5, 3.5, 0, 1), + (ad_math.smooth_max, 3.5, 4.3, 4.3, 0, 1), + (ad_math.smooth_max, 4.3, 3.5, 4.3, 1, 0), + (ad_math.remainder, 5, 2, math.remainder(5, 2), 1, -2), + (ad_math.fmod, 6, 2, math.fmod(6, 3), 1, -3), +] + +_binary_with_scalar_funcs = [ + (ad_math.pow, math.pow), + (ad_math.min, min), + (ad_math.max, max), + (ad_math.fmin, min), + (ad_math.fmax, max), + (ad_math.atan2, math.atan2), + (ad_math.remainder, math.remainder), + (ad_math.copysign, math.copysign), +] + +if sys.version_info.major > 3 or (sys.version_info.major == 3 and sys.version_info.minor >= 9): + # introduced in Python 3.9 + PARAMETERS_FOR_BINARY_FUNC.append((ad_math.nextafter, 3.5, 4.3, math.nextafter(3.5, 4.3), 1, 0)) + _binary_with_scalar_funcs.append((ad_math.nextafter, math.nextafter)) + + +@pytest.mark.parametrize("func,x,y,xd", PARAMETERS_FOR_UNARY_FUNC) +def test_unary_math_functions_for_adj(func, x, y, xd): + assert func(x) == pytest.approx(y) + x_ad = Areal(x) + + with Tape() as tape: + tape.registerInput(x_ad) + tape.newRecording() + + y_ad = func(x_ad) + tape.registerOutput(y_ad) + y_ad.setDerivative(1.0) + + tape.computeAdjoints() + + assert y_ad == pytest.approx(y) + assert x_ad.getDerivative() == pytest.approx(xd) + + +@pytest.mark.parametrize("func,x,y,yd", PARAMETERS_FOR_UNARY_FUNC) +def test_unary_math_functions_for_fwd(func, x, y, yd): + x_ad = Freal(x) + x_ad.setDerivative(1.0) + + y_ad = func(x_ad) + + assert y_ad == pytest.approx(y) + assert y_ad.getDerivative() == pytest.approx(yd) + + +@pytest.mark.parametrize("ad_func, func", _binary_with_scalar_funcs) +@pytest.mark.parametrize("value", [3, 3.1]) +def test_binary_function_with_scalar_param(value, ad_func, func): + assert ad_func(4.1, value) == pytest.approx(func(4.1, value)) + assert ad_func(4, value) == pytest.approx(func(4, value)) + assert ad_func(Freal(4.1), value) == pytest.approx(func(4.1, value)) + assert ad_func(Freal(4), value) == pytest.approx(func(4, value)) + assert ad_func(value, Freal(4.1)) == pytest.approx(func(value, 4.1)) + assert ad_func(value, Freal(4)) == pytest.approx(func(value, 4)) + + +@pytest.mark.parametrize( + "func, y, derv", + [ + (0, math.pow(4, 3), pytest.approx(3 * math.pow(4, 3 - 1))), + (1, math.pow(3, 4), pytest.approx(math.log(3) * math.pow(3, 4))), + ], +) +def test_pow_for_adj(func, y, derv): + x_ad = Areal(4.0) + with Tape() as tape: + tape.registerInput(x_ad) + tape.newRecording() + + if func == 0: + y_ad = ad_math.pow(x_ad, 3) + else: + y_ad = ad_math.pow(3, x_ad) + + tape.registerOutput(y_ad) + y_ad.setDerivative(1.0) + + tape.computeAdjoints() + + assert y_ad == pytest.approx(y) + assert x_ad.getDerivative() == pytest.approx(derv) + + +@pytest.mark.parametrize( + "func, y, derv", + [ + (0, math.pow(4, 3), pytest.approx(3 * math.pow(4, 3 - 1))), + (1, math.pow(3, 4), pytest.approx(math.log(3) * math.pow(3, 4))), + ], +) +def test_pow_for_fwd(func, y, derv): + x_ad = Freal(4.0) + x_ad.setDerivative(1.0) + + if func == 0: + y_ad = ad_math.pow(x_ad, 3) + else: + y_ad = ad_math.pow(3, x_ad) + + assert y_ad == y + assert y_ad.getDerivative() == pytest.approx(derv) + + +@pytest.mark.parametrize("func,x1, x2,y,xd1, xd2", PARAMETERS_FOR_BINARY_FUNC) +def test_binary_math_functions_for_adj(func, x1, x2, y, xd1, xd2): + x1_ad = Areal(x1) + x2_ad = Areal(x2) + with Tape() as tape: + tape.registerInput(x1_ad) + tape.registerInput(x2_ad) + tape.newRecording() + y_ad = func(x1_ad, x2_ad) + tape.registerOutput(y_ad) + y_ad.setDerivative(1.0) + + tape.computeAdjoints() + + assert y_ad == pytest.approx(y) + assert x1_ad.getDerivative() == pytest.approx(xd1) + assert x2_ad.getDerivative() == pytest.approx(xd2) + + +@pytest.mark.parametrize("func,x1, x2,y,xd1, xd2", PARAMETERS_FOR_BINARY_FUNC) +@pytest.mark.parametrize("deriv", [1, 2]) +def test_binary_math_functions_for_fwd(func, x1, x2, y, xd1, xd2, deriv): + x1_ad = Freal(x1) + x2_ad = Freal(x2) + if deriv == 1: + x1_ad.setDerivative(1.0) + else: + x2_ad.setDerivative(1.0) + + y_ad = func(x1_ad, x2_ad) + + assert y_ad == pytest.approx(y) + if deriv == 1: + assert y_ad.getDerivative() == pytest.approx(xd1) + else: + assert y_ad.getDerivative() == pytest.approx(xd2) + + +@pytest.mark.parametrize( + "func,x,y,xd", + [ + (ad_math.modf, 3.23, math.modf(3.23), 1), + (ad_math.frexp, 3, math.frexp(3), 1 / math.pow(2, 2)), + ], +) +def test_modf_frexp_functions_for_adj(func, x, y, xd): + x_ad = Areal(x) + with Tape() as tape: + tape.registerInput(x_ad) + tape.newRecording() + + y_ad = func(x_ad) + tape.registerOutput(y_ad[0]) + y_ad[0].setDerivative(1.0) + + tape.computeAdjoints() + + assert y_ad == pytest.approx(y) + assert x_ad.getDerivative() == pytest.approx(xd) + + +@pytest.mark.parametrize( + "func,x,y,xd", + [ + (ad_math.modf, 3.23, math.modf(3.23), 1), + (ad_math.frexp, 3, math.frexp(3), 1 / math.pow(2, 2)), + ], +) +def test_modf_frexp_functions_for_fwd(func, x, y, xd): + x_ad = Freal(x) + x_ad.setDerivative(1.0) + + y_ad = func(x_ad) + + assert y_ad == pytest.approx(y) + assert y_ad[0].getDerivative() == pytest.approx(xd) + + +@pytest.mark.parametrize( + "func, y, xd", + [ + (ad_math.degrees, math.degrees(3), 180 / math.pi), + (ad_math.radians, math.radians(3), math.pi / 180), + ], +) +def test_degrees_radians_adj(func, y, xd): + with Tape() as tape: + x_ad = Areal(3.0) + tape.registerInput(x_ad) + tape.newRecording() + + y_ad = func(x_ad) + tape.registerOutput(y_ad) + y_ad.setDerivative(1.0) + + tape.computeAdjoints() + + assert y_ad.getValue() == pytest.approx(y) + assert x_ad.getDerivative() == pytest.approx(xd) + + +@pytest.mark.parametrize( + "func, y, xd", + [ + (ad_math.degrees, math.degrees(3), 180 / math.pi), + (ad_math.radians, math.radians(3), math.pi / 180), + ], +) +def test_degrees_radians_fwd(func, y, xd): + x_ad = Freal(3) + x_ad.setDerivative(1.0) + + y_ad = func(x_ad) + + assert y_ad.getValue() == pytest.approx(y) + assert y_ad.getDerivative() == pytest.approx(xd) + + +@pytest.mark.parametrize("Real", [Areal, Freal]) +def test_copysign(Real): + assert ad_math.copysign(Real(-3.1), 4) == pytest.approx(math.copysign(-3.1, 4)) + assert ad_math.copysign(Real(4), -3.1) == pytest.approx(math.copysign(4, -3.1)) + assert ad_math.copysign(Real(-3.1), Real(4)) == pytest.approx(math.copysign(-3.1, 4)) + + +def test_copysign_derivative_for_adj(): + with Tape() as tape: + x1_ad = Areal(-3.1) + x2_ad = Areal(4) + tape.registerInput(x1_ad) + tape.registerInput(x2_ad) + tape.newRecording() + + y_ad = ad_math.copysign(x1_ad, x2_ad) + tape.registerOutput(y_ad) + y_ad.setDerivative(1.0) + + tape.computeAdjoints() + + assert y_ad == pytest.approx(math.copysign(-3.1, 4)) + assert x1_ad.getDerivative() == pytest.approx(-1) + assert x2_ad.getDerivative() == pytest.approx(0) + + +@pytest.mark.parametrize("deriv", [1, 2]) +def test_copysign_derivative_for_fwd(deriv): + x1_ad = Freal(-3.1) + x2_ad = Freal(4) + if deriv == 1: + x1_ad.setDerivative(1.0) + else: + x2_ad.setDerivative(1.0) + + y_ad = ad_math.copysign(x1_ad, x2_ad) + + assert y_ad == pytest.approx(math.copysign(-3.1, 4)) + if deriv == 1: + assert y_ad.getDerivative() == pytest.approx(-1) + else: + assert y_ad.getDerivative() == pytest.approx(0) + + +def test_sum_adj(): + with Tape() as tape: + x1_ad = Areal(-3.1) + x2_ad = Areal(4) + x3_ad = Areal(2.4) + tape.registerInput(x1_ad) + tape.registerInput(x2_ad) + tape.registerInput(x3_ad) + tape.newRecording() + + y_ad = sum([x1_ad, x2_ad, x3_ad]) + tape.registerOutput(y_ad) + y_ad.setDerivative(1.0) + + tape.computeAdjoints() + + assert y_ad == pytest.approx(3.3) + assert x1_ad.getDerivative() == pytest.approx(1) + assert x2_ad.getDerivative() == pytest.approx(1) + assert x3_ad.getDerivative() == pytest.approx(1) + + +@pytest.mark.parametrize("deriv", [1, 2]) +def test_sum_for_fwd(deriv): + x1_ad = Freal(-3.1) + x2_ad = Freal(4) + if deriv == 1: + x1_ad.setDerivative(1.0) + else: + x2_ad.setDerivative(1.0) + + y_ad = sum([x1_ad, x2_ad]) + + assert y_ad == pytest.approx(sum([-3.1, 4])) + assert y_ad.getDerivative() == pytest.approx(1) + + +def test_hypot_adj(): + with Tape() as tape: + x1_ad = Areal(-3.1) + x2_ad = Areal(4) + tape.registerInput(x1_ad) + tape.registerInput(x2_ad) + tape.newRecording() + + y_ad = ad_math.hypot(x1_ad, x2_ad) + tape.registerOutput(y_ad) + y_ad.setDerivative(1.0) + + tape.computeAdjoints() + + assert y_ad == pytest.approx(math.hypot(-3.1, 4)) + assert x1_ad.getDerivative() == pytest.approx(-3.1 / math.hypot(-3.1, 4)) + assert x2_ad.getDerivative() == pytest.approx(4 / math.hypot(-3.1, 4)) + + +@pytest.mark.parametrize("deriv", [1, 2]) +def test_hypot_for_fwd(deriv): + x1_ad = Freal(-3.1) + x2_ad = Freal(4) + if deriv == 1: + x1_ad.setDerivative(1.0) + else: + x2_ad.setDerivative(1.0) + + y_ad = ad_math.hypot(x1_ad, x2_ad) + + assert y_ad == pytest.approx(math.hypot(-3.1, 4)) + if deriv == 1: + assert y_ad.getDerivative() == pytest.approx(-3.1 / math.hypot(-3.1, 4)) + else: + assert y_ad.getDerivative() == pytest.approx(4 / math.hypot(-3.1, 4)) + + +def test_dist_for_adj(): + with Tape() as tape: + x1_ad = Areal(-3.1) + x2_ad = Areal(4) + x3_ad = Areal(2.4) + x4_ad = Areal(1) + tape.registerInput(x1_ad) + tape.registerInput(x2_ad) + tape.registerInput(x3_ad) + tape.registerInput(x4_ad) + tape.newRecording() + + y_ad = ad_math.dist([x1_ad, x2_ad], [x3_ad, x4_ad]) + tape.registerOutput(y_ad) + y_ad.setDerivative(1.0) + + tape.computeAdjoints() + + assert y_ad == pytest.approx(math.dist([-3.1, 4], [2.4, 1])) + assert x1_ad.getDerivative() == pytest.approx(-5.5 / math.dist([-3.1, 4], [2.4, 1])) + assert x2_ad.getDerivative() == pytest.approx(3 / math.dist([-3.1, 4], [2.4, 1])) + assert x3_ad.getDerivative() == pytest.approx(5.5 / math.dist([-3.1, 4], [2.4, 1])) + assert x4_ad.getDerivative() == pytest.approx(-3 / math.dist([-3.1, 4], [2.4, 1])) + + +@pytest.mark.parametrize("deriv", [1, 2, 3, 4]) +def test_dist_for_fwd(deriv): + x1_ad = Freal(-3.1) + x2_ad = Freal(4) + x3_ad = Freal(2.4) + x4_ad = Freal(1) + if deriv == 1: + x1_ad.setDerivative(1.0) + elif deriv == 2: + x2_ad.setDerivative(1.0) + elif deriv == 3: + x3_ad.setDerivative(1.0) + else: + x4_ad.setDerivative(1.0) + + y_ad = ad_math.dist([x1_ad, x2_ad], [x3_ad, x4_ad]) + + assert y_ad == pytest.approx(math.dist([-3.1, 4], [2.4, 1])) + if deriv == 1: + assert y_ad.getDerivative() == pytest.approx(-5.5 / math.dist([-3.1, 4], [2.4, 1])) + elif deriv == 2: + assert y_ad.getDerivative() == pytest.approx(3 / math.dist([-3.1, 4], [2.4, 1])) + elif deriv == 3: + assert y_ad.getDerivative() == pytest.approx(5.5 / math.dist([-3.1, 4], [2.4, 1])) + else: + assert y_ad.getDerivative() == pytest.approx(-3 / math.dist([-3.1, 4], [2.4, 1])) diff --git a/bindings/python/tests/test_real_operations.py b/bindings/python/tests/test_real_operations.py new file mode 100644 index 00000000..5437c19f --- /dev/null +++ b/bindings/python/tests/test_real_operations.py @@ -0,0 +1,645 @@ +############################################################################## +# +# Pytests for operations and derivatives on the active types +# +# This file is part of XAD, a comprehensive C++ library for +# automatic differentiation. +# +# Copyright (C) 2010-2024 Xcelerit Computing Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from pytest import approx, raises +import pytest +from xad_autodiff.adj_1st import Real as AReal, Tape +from xad_autodiff.fwd_1st import Real as FReal +from xad_autodiff import value, derivative +import math as m + + +# This is a list of math functions with their expected outcomes and derivatives, +# used in parametrised tests, for binary arithmetic functions with one active operand. +# +# The format is a list of tuples, where each tuple has the following entries: +# - math function: Callable (lambda), with one parameter +# - parameter1 value for the function: float +# - expected result: float +# - expected derivative1 value: float +# +PARAMETERS_FOR_BINARY_ARITHMETICS_1_ACTIVE_OPERAND = [ + (lambda a: 2 * a, 3, 6, 2), + (lambda a: 2 + a, 3, 5, 1), + (lambda a: 2 - a, 3, -1, -1), + (lambda a: 2 / a, 3, 2 / 3, -2 / 9), + (lambda a: a * 3.6, 3, 10.8, 3.6), + (lambda a: a + 3.9, 3, 6.9, 1), + (lambda a: a - 4.3, 3, -1.3, 1), + (lambda a: a / 2, 3, 1.5, 1 / 2), +] + +# This is a list of math functions with their expected outcomes and derivatives, +# used in parametrised tests, for unary arithmetic functions (+x, -x). +# +# The format is a list of tuples, where each tuple has the following entries: +# - math function: Callable (lambda), with one parameter +# - parameter1 value for the function: float +# - expected result: float +# - expected derivative1 value: float +# +PARAMETERS_FOR_UNARY_ARITHMETICS = [(lambda a: +a, 3, 3, 1), (lambda a: -a, 3, -3, -1)] + +# This is a list of math functions with their expected outcomes and derivatives, +# used in parametrised tests, for binary arithmetic functions with two active operands. +# +# The format is a list of tuples, where each tuple has the following entries: +# - math function: Callable (lambda, 2 parameters) +# - parameter1 value for the function: float +# - parameter2 value for the function: float +# - expected result: float +# - expected derivative1 value: float +# - expected derivative2 value: float +# +PARAMETERS_FOR_BINARY_ARITHMETICS_2_ACTIVE_OPERANDS = [ + (lambda a, b: a * b, 5.0, 2.0, 10, 2, 5), + (lambda a, b: a + b, 5.0, 2.0, 7, 1, 1), + (lambda a, b: a - b, 5.0, 2.0, 3, 1, -1), + (lambda a, b: a / b, 5.0, 2.0, 2.5, 0.5, -1.25), +] + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_initialize_from_float(ad_type): + assert ad_type(0.3).getValue() == approx(0.3) + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_initialize_from_int(ad_type): + assert ad_type(1).getValue() == 1 + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_add_float(ad_type): + real = ad_type(0.4) + 0.3 + assert real.getValue() == approx(0.7) + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_add_int(ad_type): + real = ad_type(1) + 2 + assert real.getValue() == 3 + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_sub_float(ad_type): + real = ad_type(0.3) - 0.4 + assert real.getValue() == approx(-0.1) + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_sub_int(ad_type): + real = ad_type(1) - 2 + assert real.getValue() == -1 + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_add_to_float(ad_type): + real = 0.3 + ad_type(0.4) + assert real.getValue() == approx(0.7) + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_sub_to_float(ad_type): + real = 2.5 - ad_type(2.0) + assert real.getValue() == approx(0.5) + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_add_to_int(ad_type): + real = 2 + ad_type(1) + assert real.getValue() == 3 + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_sub_to_int(ad_type): + real = 2 - ad_type(1) + assert real.getValue() == 1 + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_add_real(ad_type): + real = ad_type(2) + ad_type(1) + assert real.getValue() == 3 + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_sub_real(ad_type): + real = ad_type(2) - ad_type(1) + assert real.getValue() == 1 + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_mul_float(ad_type): + real = ad_type(0.2) * 0.5 + assert real.getValue() == approx(0.1) + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_mul_int(ad_type): + real = ad_type(1) * 2 + assert real.getValue() == 2 + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_mul_to_float(ad_type): + real = 0.5 * ad_type(0.2) + assert real.getValue() == approx(0.1) + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_mul_to_int(ad_type): + real = 2 * ad_type(1) + assert real.getValue() == 2 + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_mul_real(ad_type): + real = ad_type(0.2) * ad_type(0.5) + assert real.getValue() == approx(0.1) + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_div_float(ad_type): + real = ad_type(0.2) / 0.5 + assert real.getValue() == approx(0.4) + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_div_int(ad_type): + real = ad_type(1) / 2 + assert real.getValue() == approx(0.5) + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_div_to_float(ad_type): + real = 0.5 / ad_type(0.2) + assert real.getValue() == approx(2.5) + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_div_to_int(ad_type): + real = 2 / ad_type(1) + assert real.getValue() == 2 + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_div_real(ad_type): + real = ad_type(0.2) / ad_type(0.5) + assert real.getValue() == approx(0.4) + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_addition_assignment_int(ad_type): + real = ad_type(0.2) + real += 1 + assert real.getValue() == approx(1.2) + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_addition_assignment_float(ad_type): + real = ad_type(0.2) + real += 1.9 + assert real.getValue() == approx(2.1) + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_addition_assignment_real(ad_type): + real = ad_type(0.2) + real += ad_type(0.5) + assert real.getValue() == approx(0.7) + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_sub_assignment_int(ad_type): + real = ad_type(0.2) + real -= 1 + assert real.getValue() == approx(-0.8) + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_sub_assignment_float(ad_type): + real = ad_type(0.2) + real -= 1.9 + assert real.getValue() == approx(-1.7) + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_sub_assignment_real(ad_type): + real = ad_type(0.2) + real -= ad_type(0.5) + assert real.getValue() == approx(-0.3) + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_comparison_real_to_real(ad_type): + a = ad_type(0.2) + b = ad_type(0.5) + assert (b > a) is True + assert (a > b) is False + assert (b >= a) is True + assert (a >= b) is False + assert (b < a) is False + assert (a < b) is True + assert (b <= a) is False + assert (a <= b) is True + c = ad_type(0.2) + assert (a != b) is True + assert (a != c) is False + assert (a == c) is True + assert (b == c) is False + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_comparison_real_to_float(ad_type): + a = ad_type(0.2) + b = 0.5 + assert (b > a) is True + assert (a > b) is False + assert (b >= a) is True + assert (a >= b) is False + assert (b < a) is False + assert (a < b) is True + assert (b <= a) is False + assert (a <= b) is True + c = 0.2 + assert (a != b) is True + assert (a != c) is False + assert (a == c) is True + assert (b == c) is False + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_comparison_real_to_int(ad_type): + a = ad_type(2) + b = 5 + assert (b > a) is True + assert (a > b) is False + assert (b >= a) is True + assert (a >= b) is False + assert (b < a) is False + assert (a < b) is True + assert (b <= a) is False + assert (a <= b) is True + c = 2 + assert (a != b) is True + assert (a != c) is False + assert (a == c) is True + assert (b == c) is False + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_rounding(ad_type): + assert round(ad_type(2.345), 2) == pytest.approx(2.35) + assert round(ad_type(2.345), 1) == pytest.approx(2.3) + assert round(ad_type(2.345), 0) == pytest.approx(2.0) + assert round(ad_type(2.345)) == pytest.approx(2.0) + assert type(round(ad_type(2.3))) == type(round(2.3)) + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +@pytest.mark.parametrize( + "func", [m.ceil, m.floor, m.trunc, int], ids=["ceil", "floor", "trunc", "int"] +) +def test_truncation_funcs(ad_type, func): + assert func(ad_type(2.345)) == func(2.345) + assert func(ad_type(2.845)) == func(2.845) + assert func(ad_type(-2.845)) == func(-2.845) + assert func(ad_type(0.0)) == func(0.0) + assert isinstance(func(ad_type(1.1)), int) + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_abs(ad_type): + assert abs(ad_type(2.345)) == pytest.approx(2.345) + assert abs(ad_type(-2.345)) == pytest.approx(2.345) + assert abs(ad_type(0.0)) == 0.0 + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_bool(ad_type): + assert bool(ad_type(1.0)) is bool(1.0) + assert bool(ad_type(0.0)) is bool(0.0) + assert bool(ad_type(1.0)) is bool(-1.0) + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_mod(ad_type): + assert ad_type(2.7) % 2 == 2.7 % 2 + assert ad_type(2.7) % ad_type(2.0) == 2.7 % 2.0 + assert 2.7 % ad_type(2.0) == 2.7 % 2.0 + assert 2 % ad_type(2.0) == 2 % 2.0 + assert ad_type(-2.7) % 2 == -2.7 % 2 + assert ad_type(-2.7) % ad_type(2.0) == -2.7 % 2.0 + assert -2.7 % ad_type(2.0) == -2.7 % 2.0 + assert -2 % ad_type(2.0) == -2 % 2.0 + assert ad_type(2.7) % -2 == 2.7 % -2 + assert ad_type(2.7) % ad_type(-2.0) == 2.7 % -2.0 + assert 2.7 % ad_type(-2.0) == 2.7 % -2.0 + assert 2 % ad_type(-2.0) == 2 % -2.0 + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_divmod(ad_type): + assert divmod(ad_type(2.7), 2) == divmod(2.7, 2) + assert divmod(ad_type(2.7), ad_type(2.0)) == divmod(2.7, 2.0) + assert divmod(2.7, ad_type(2.0)) == divmod(2.7, 2.0) + assert divmod(2, ad_type(2.0)) == divmod(2, 2.0) + assert divmod(ad_type(-2.7), 2) == divmod(-2.7, 2) + assert divmod(ad_type(-2.7), ad_type(2.0)) == divmod(-2.7, 2.0) + assert divmod(-2.7, ad_type(2.0)) == divmod(-2.7, 2.0) + assert divmod(-2, ad_type(2.0)) == divmod(-2, 2.0) + assert divmod(ad_type(2.7), -2) == divmod(2.7, -2) + assert divmod(ad_type(2.7), ad_type(-2.0)) == divmod(2.7, -2.0) + assert divmod(2.7, ad_type(-2.0)) == divmod(2.7, -2.0) + assert divmod(2, ad_type(-2.0)) == divmod(2, -2.0) + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_floordiv(ad_type): + assert ad_type(2.7) // 2 == 2.7 // 2 + assert ad_type(2.7) // ad_type(2.0) == 2.7 // 2.0 + assert 2.7 // ad_type(2.0) == 2.7 // 2.0 + assert 2 // ad_type(2.0) == 2 // 2.0 + assert ad_type(-2.7) // 2 == -2.7 // 2 + assert ad_type(-2.7) // ad_type(2.0) == -2.7 // 2.0 + assert -2.7 // ad_type(2.0) == -2.7 // 2.0 + assert -2 // ad_type(2.0) == -2 // 2.0 + assert ad_type(2.7) // -2 == 2.7 // -2 + assert ad_type(2.7) // ad_type(-2.0) == 2.7 // -2.0 + assert 2.7 // ad_type(-2.0) == 2.7 // -2.0 + assert 2 // ad_type(-2.0) == 2 // -2.0 + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_pow_operator(ad_type): + assert ad_type(2.7) ** 2 == pytest.approx(2.7**2) + assert ad_type(2.7) ** 2.4 == pytest.approx(2.7**2.4) + assert ad_type(2.7) ** ad_type(2.4) == pytest.approx(2.7**2.4) + assert 2.7 ** ad_type(2.4) == pytest.approx(2.7**2.4) + assert 2 ** ad_type(2.4) == pytest.approx(2**2.4) + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_hash_method(ad_type): + assert hash(ad_type(2.7)) == hash(2.7) + assert hash(ad_type(-2.7)) == hash(-2.7) + assert hash(ad_type(0)) == hash(0) + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_getnewargs_method(ad_type): + assert ad_type(1.2).__getnewargs__() == (1.2,) + assert ad_type(1).__getnewargs__() == (1.0,) + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_as_integer_ratio(ad_type): + assert ad_type(1.2).as_integer_ratio() == (1.2).as_integer_ratio() + assert ad_type(-21.2).as_integer_ratio() == (-21.2).as_integer_ratio() + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_conjugate(ad_type): + assert ad_type(1.2).conjugate() == (1.2).conjugate() + assert ad_type(-21.2).conjugate() == (-21.2).conjugate() + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_fromhex(ad_type): + assert ad_type.fromhex("0x3.a7p10") == float.fromhex("0x3.a7p10") + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_hex(ad_type): + assert ad_type(1.23).hex() == (1.23).hex() + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_imag(ad_type): + assert ad_type(1.23).imag() == pytest.approx(0.0) + assert ad_type(-1.23).imag() == pytest.approx(0.0) + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_real(ad_type): + assert ad_type(1.23).real() == pytest.approx(1.23) + assert ad_type(-1.23).real() == pytest.approx(-1.23) + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_isinteger(ad_type): + assert ad_type(1.23).is_integer() is False + assert ad_type(-1.23).is_integer() is False + assert ad_type(21.0).is_integer() is True + assert ad_type(-1234.0).is_integer() is True + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_format(ad_type): + assert f"{ad_type(1.23):10.5f}" == f"{1.23:10.5f}" + assert "{:10.5f}".format(ad_type(1.23)) == "{:10.5f}".format(1.23) + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_value_function(ad_type): + assert value(3) == 3 + assert value(3.2) == approx(3.2) + assert value("3") == "3" + assert value(ad_type(3.1)) == approx(3.1) + + +@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"]) +def test_value_property_get(ad_type): + assert ad_type(3.1).value == approx(3.1) + + +def test_derivative_property_get_fwd(): + x = FReal(1.2) + x.setDerivative(1.0) + assert x.derivative == 1.0 + + +def test_derivative_property_set_fwd(): + x = FReal(1.2) + x.derivative = 1.0 + assert x.getDerivative() == 1.0 + + +def test_derivative_property_get_adj(): + x = AReal(1.2) + with Tape() as tape: + tape.registerInput(x) + tape.newRecording() + y = x + tape.registerOutput(y) + y.setDerivative(1.0) + assert y.derivative == 1.0 + + +def test_derivative_property_set_adj(): + x = AReal(1.2) + with Tape() as tape: + tape.registerInput(x) + tape.newRecording() + y = x + tape.registerOutput(y) + y.derivative = 1.0 + assert y.getDerivative() == 1.0 + + +def test_derivative_function(): + x = AReal(3.2) + + with Tape() as tape: + tape.registerInput(x) + tape.newRecording() + + y_ad = x + tape.registerOutput(y_ad) + y_ad.setDerivative(1.0) + + tape.computeAdjoints() + assert derivative(x) == x.getDerivative() + with raises(TypeError): + derivative(1) + + +def test_should_record(): + x = AReal(42.0) + assert x.shouldRecord() is False + with Tape() as tape: + tape.registerInput(x) + assert x.shouldRecord() is True + + +def test_set_adjoint(): + x = AReal(42.0) + with Tape() as tape: + tape.registerInput(x) + tape.newRecording() + y = 4 * x + tape.registerOutput(x) + y.setAdjoint(1.0) + tape.computeAdjoints() + assert derivative(x) == 4.0 + + +@pytest.mark.parametrize("func,x,y,xd", PARAMETERS_FOR_UNARY_ARITHMETICS) +def test_unary_arithmetics_adj(func, x, y, xd): + x_ad = AReal(x) + + with Tape() as tape: + tape.registerInput(x_ad) + tape.newRecording() + + y_ad = func(x_ad) + tape.registerOutput(y_ad) + y_ad.setDerivative(1.0) + + tape.computeAdjoints() + + assert y_ad.getValue() == pytest.approx(y) + assert x_ad.getDerivative() == pytest.approx(xd) + + +@pytest.mark.parametrize("func,x,y,xd", PARAMETERS_FOR_UNARY_ARITHMETICS) +def test_unary_arithmetics_fwd(func, x, y, xd): + x_ad = FReal(x) + x_ad.setDerivative(1.0) + + y_ad = func(x_ad) + + assert y_ad == y + assert y_ad.getDerivative() == xd + + +@pytest.mark.parametrize("func,x,y,xd", PARAMETERS_FOR_BINARY_ARITHMETICS_1_ACTIVE_OPERAND) +def test_binary_arithmetics_fwd(func, x, y, xd): + x1_ad = FReal(x) + x1_ad.setDerivative(1.0) + y_ad = func(x1_ad) + assert y_ad.getValue() == pytest.approx(y) + assert y_ad.getDerivative() == pytest.approx(xd) + + +@pytest.mark.parametrize("func,x,y,xd", PARAMETERS_FOR_BINARY_ARITHMETICS_1_ACTIVE_OPERAND) +def test_binary_arithmetics_adj(func, x, y, xd): + x_ad = AReal(x) + + with Tape() as tape: + tape.registerInput(x_ad) + tape.newRecording() + + y_ad = func(x_ad) + tape.registerOutput(y_ad) + y_ad.setDerivative(1.0) + + tape.computeAdjoints() + + assert y_ad.getValue() == pytest.approx(y) + assert x_ad.getDerivative() == pytest.approx(xd) + + +@pytest.mark.parametrize( + "func,x1, x2,y,xd1, xd2", PARAMETERS_FOR_BINARY_ARITHMETICS_2_ACTIVE_OPERANDS +) +def test_binary_with_2_active_operands_adj(func, x1, x2, y, xd1, xd2): + x1_ad = AReal(x1) + x2_ad = AReal(x2) + + with Tape() as tape: + tape.registerInput(x1_ad) + tape.registerInput(x2_ad) + tape.newRecording() + + y_ad = func(x1_ad, x2_ad) + tape.registerOutput(y_ad) + y_ad.setDerivative(1.0) + + tape.computeAdjoints() + + assert y_ad == pytest.approx(y) + assert x1_ad.getDerivative() == pytest.approx(xd1) + assert x2_ad.getDerivative() == pytest.approx(xd2) + + +@pytest.mark.parametrize( + "func,x1, x2,y,xd1, xd2", PARAMETERS_FOR_BINARY_ARITHMETICS_2_ACTIVE_OPERANDS +) +@pytest.mark.parametrize("deriv", [1, 2]) +def test_binary_with_2_active_operands_fwd(func, x1, x2, y, xd1, xd2, deriv): + x1_ad = FReal(x1) + x2_ad = FReal(x2) + if deriv == 1: + x1_ad.setDerivative(1.0) + else: + x2_ad.setDerivative(1.0) + + y_ad = func(x1_ad, x2_ad) + assert y_ad.getValue() == pytest.approx(y) + if deriv == 1: + assert y_ad.getDerivative() == pytest.approx(xd1) + else: + assert y_ad.getDerivative() == pytest.approx(xd2) diff --git a/bindings/python/tests/test_tape.py b/bindings/python/tests/test_tape.py new file mode 100644 index 00000000..873cb2a7 --- /dev/null +++ b/bindings/python/tests/test_tape.py @@ -0,0 +1,149 @@ +############################################################################## +# +# Test the adjoint tape in Python. +# +# This file is part of XAD, a comprehensive C++ library for +# automatic differentiation. +# +# Copyright (C) 2010-2024 Xcelerit Computing Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + + +import pytest +from xad_autodiff import derivative, exceptions, value +from xad_autodiff.adj_1st import Tape, Real + + +def test_active_tape(): + tape = Tape() + assert tape.isActive() is False + tape.activate() + assert tape.isActive() is True + tape.deactivate() + assert tape.isActive() is False + + +def test_tape_using_with(): + with Tape() as tape: + assert tape.isActive() is True + tape = Tape() + assert tape.isActive() is False + with tape: + assert tape.isActive() is True + assert tape.isActive() is False + + +def test_get_active(): + t = Tape() + assert Tape.getActive() is None + t.activate() + assert Tape.getActive() is not None + assert Tape.getActive() == t + + +def test_get_position(): + with Tape() as t: + assert t.getPosition() == 0 + x1 = Real(1.0) + t.registerInput(x1) + x2 = 1.2 * x1 + x1.setDerivative(1.0) + t.registerOutput(x2) + t.computeAdjoints() + assert t.getPosition() >= 0 + + +def test_clear_derivative_after(): + with Tape() as tape: + x1 = Real(1.0) + tape.registerInput(x1) + x2 = 1.2 * x1 + pos = tape.getPosition() + x3 = 1.4 * x2 * x1 + x4 = x2 + x3 + tape.registerOutput(x4) + x4.setDerivative(1.0) + x3.setDerivative(1.0) + x2.setDerivative(1.0) + x1.setDerivative(1.0) + tape.clearDerivativesAfter(pos) + + assert derivative(x2) == 1.0 + assert derivative(x1) == 1.0 + with pytest.raises(exceptions.OutOfRange) as e: + derivative(x3) + assert "given derivative slot is out of range - did you register the outputs?" in str(e) + with pytest.raises(exceptions.OutOfRange) as e: + derivative(x4) + assert "given derivative slot is out of range - did you register the outputs?" in str(e) + + +def test_reset_to_and_compute_adjoints_to_usage(): + i = Real(2.0) + with Tape() as tape: + tape.registerInput(i) + tape.newRecording() + pos = tape.getPosition() + values = [] + deriv = [] + for p in range(1, 10): + v = p * i + tape.registerOutput(v) + v.setDerivative(1.0) + tape.computeAdjointsTo(pos) + values.append(value(v)) + deriv.append(derivative(i)) + tape.resetTo(pos) + tape.clearDerivatives() + + assert values == [2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0, 18.0] + assert deriv == [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0] + + +def test_derivative(): + with Tape() as t: + x = Real(1.0) + t.registerInput(x) + assert t.derivative(x) == 0.0 + + +def test_get_derivative(): + with Tape() as t: + x = Real(1.0) + t.registerInput(x) + assert t.getDerivative(x) == 0.0 + + +def test_set_derivative_value(): + with Tape() as t: + x = Real(1.0) + t.registerInput(x) + t.setDerivative(x, 1.0) + assert t.derivative(x) == 1.0 + with pytest.raises(exceptions.OutOfRange): + derivative(t.setDerivative(1231, 0.0)) + + +def test_set_derivative_slot(): + with Tape() as t: + x = Real(1.0) + t.registerInput(x) + slot = x.getSlot() + assert isinstance(slot, int) + t.setDerivative(slot, 1.0) + assert t.derivative(x) == 1.0 + assert t.getDerivative(slot) == 1.0 diff --git a/bindings/python/xad_autodiff/__init__.py b/bindings/python/xad_autodiff/__init__.py new file mode 100644 index 00000000..d79ebd43 --- /dev/null +++ b/bindings/python/xad_autodiff/__init__.py @@ -0,0 +1,60 @@ +############################################################################## +# +# XAD Python bindings +# +# This file is part of XAD, a comprehensive C++ library for +# automatic differentiation. +# +# Copyright (C) 2010-2024 Xcelerit Computing Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +"""Python bindings for the XAD comprehensive library for automatic differentiation""" + +from typing import Any, Union +from ._xad_autodiff import adj_1st, fwd_1st + +__all__ = ["value", "derivative"] + + +def value(x: Union[adj_1st.Real, fwd_1st.Real, Any]) -> float: + """Get the value of an XAD active type - or return the value itself otherwise + + Args: + x (Real | any): Argument to get the value of + + Returns: + float: The value stored in the variable + """ + if isinstance(x, adj_1st.Real) or isinstance(x, fwd_1st.Real): + return x.getValue() + else: + return x + + +def derivative(x: Union[adj_1st.Real, fwd_1st.Real]) -> float: + """Get the derivative of an XAD active type - forward or adjoint mode + + Args: + x (Real): Argument to extract the derivative information from + + Returns: + float: The derivative + """ + if isinstance(x, adj_1st.Real) or isinstance(x, fwd_1st.Real): + return x.getDerivative() + else: + raise TypeError("type " + type(x).__name__ + " is not an XAD active type") diff --git a/bindings/python/xad_autodiff/adj_1st/__init__.py b/bindings/python/xad_autodiff/adj_1st/__init__.py new file mode 100644 index 00000000..f8008016 --- /dev/null +++ b/bindings/python/xad_autodiff/adj_1st/__init__.py @@ -0,0 +1,105 @@ +############################################################################## +# +# First order adjoint mode module for the XAD Python bindings +# +# This file is part of XAD, a comprehensive C++ library for +# automatic differentiation. +# +# Copyright (C) 2010-2024 Xcelerit Computing Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from typing import Tuple, Type +from xad_autodiff._xad_autodiff.adj_1st import Real, Tape + +__all__ = ["Real", "Tape"] + + +def _register_inputs(self, inputs): + for i in inputs: + self.registerInput(i) + + +Tape.registerInputs = _register_inputs + + +def _register_outputs(self, outputs): + for o in outputs: + self.registerOutput(o) + + +Tape.registerOutputs = _register_outputs + +setattr(Real, "value", property(Real.getValue, doc="get the underlying float value of the object")) +setattr( + Real, + "derivative", + property( + Real.getDerivative, Real.setDerivative, doc="get/set the derivative (adjoint) of the object" + ), +) + + +# additional methods inserted on the python side +def _as_integer_ratio(x: Real) -> Tuple[int, int]: + """Returns a rational representation of the float with numerator and denominator in a tuple""" + return x.value.as_integer_ratio() + + +Real.as_integer_ratio = _as_integer_ratio + + +def _fromhex(cls: Type[Real], hexstr: str) -> Real: + """Initialize from a hex expression""" + return cls(float.fromhex(hexstr)) + + +Real.fromhex = classmethod(_fromhex) + + +def _getnewargs(x: Real) -> Tuple[float]: + return (x.value,) + + +Real.__getnewargs__ = _getnewargs + + +def _hash(x: Real) -> int: + return hash(x.value) + + +Real.__hash__ = _hash + + +def _hex(x: Real) -> str: + return x.value.hex() + + +Real.hex = _hex + + +def _is_integer(x: Real) -> bool: + return x.value.is_integer() + + +Real.is_integer = _is_integer + + +def _format(x: Real, spec) -> str: + return format(x.value, spec) + + +Real.__format__ = _format diff --git a/bindings/python/xad_autodiff/exceptions/__init__.py b/bindings/python/xad_autodiff/exceptions/__init__.py new file mode 100644 index 00000000..d3ca7286 --- /dev/null +++ b/bindings/python/xad_autodiff/exceptions/__init__.py @@ -0,0 +1,39 @@ +############################################################################## +# +# Exceptions module for the XAD Python bindings +# +# This file is part of XAD, a comprehensive C++ library for +# automatic differentiation. +# +# Copyright (C) 2010-2024 Xcelerit Computing Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from xad_autodiff._xad_autodiff.exceptions import ( + XadException, + TapeAlreadyActive, + OutOfRange, + DerivativesNotInitialized, + NoTapeException, +) + +__all__ = [ + "XadException", + "TapeAlreadyActive", + "OutOfRange", + "DerivativesNotInitialized", + "NoTapeException", +] diff --git a/bindings/python/xad_autodiff/fwd_1st/__init__.py b/bindings/python/xad_autodiff/fwd_1st/__init__.py new file mode 100644 index 00000000..3e14c12b --- /dev/null +++ b/bindings/python/xad_autodiff/fwd_1st/__init__.py @@ -0,0 +1,76 @@ +############################################################################## +# +# First order forward mode module for the XAD Python bindings +# +# This file is part of XAD, a comprehensive C++ library for +# automatic differentiation. +# +# Copyright (C) 2010-2024 Xcelerit Computing Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from typing import Tuple, Type +from xad_autodiff._xad_autodiff.fwd_1st import Real + +__all__ = ["Real"] + + +setattr(Real, "value", property(Real.getValue, doc="get the underlying float value of the object")) +setattr( + Real, + "derivative", + property( + Real.getDerivative, Real.setDerivative, doc="get/set the derivative of the object" + ), +) + +# additional methods inserted on the python side +def _as_integer_ratio(x: Real) -> Tuple[int, int]: + """Returns a rational representation of the float with numerator and denominator in a tuple""" + return x.value.as_integer_ratio() + +Real.as_integer_ratio = _as_integer_ratio + +def _fromhex(cls: Type[Real], hexstr: str) -> Real: + """Initialize from a hex expression""" + return cls(float.fromhex(hexstr)) + +Real.fromhex = classmethod(_fromhex) + +def _getnewargs(x: Real) -> Tuple[float]: + return (x.value, ) + +Real.__getnewargs__ = _getnewargs + +def _hash(x: Real) -> int: + return hash(x.value) + +Real.__hash__ = _hash + +def _hex(x: Real) -> str: + return x.value.hex() + +Real.hex = _hex + +def _is_integer(x: Real) -> bool: + return x.value.is_integer() + +Real.is_integer = _is_integer + +def _format(x: Real, spec) -> str: + return format(x.value, spec) + +Real.__format__ = _format \ No newline at end of file diff --git a/bindings/python/xad_autodiff/math/__init__.py b/bindings/python/xad_autodiff/math/__init__.py new file mode 100644 index 00000000..85b48e90 --- /dev/null +++ b/bindings/python/xad_autodiff/math/__init__.py @@ -0,0 +1,175 @@ +############################################################################## +# +# Math module for the XAD Python bindings +# +# This file is part of XAD, a comprehensive C++ library for +# automatic differentiation. +# +# Copyright (C) 2010-2024 Xcelerit Computing Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +"""XAD math module - mimics the standard math module, but allows using XAD active types + as arguments. Note that it's also possible to call the functions contained with + float arguments (passive type), to allow seamless integration with active and passive + data types. +""" + +from typing import Union, List +from xad_autodiff._xad_autodiff.math import ( + sqrt, + pow, + log10, + log, + ldexp, + exp, + exp2, + expm1, + log1p, + log2, + modf, + ceil, + floor, + frexp, + fmod, + min, + max, + fmax, + fmin, + abs, + fabs, + smooth_abs, + smooth_max, + smooth_min, + tan, + atan, + tanh, + atan2, + atanh, + cos, + acos, + cosh, + acosh, + sin, + asin, + sinh, + asinh, + cbrt, + erf, + erfc, + nextafter, + remainder, + degrees, + radians, + copysign, + trunc, +) + + +__all__ = [ + "sqrt", + "pow", + "log10", + "log", + "ldexp", + "exp", + "exp2", + "expm1", + "log1p", + "log2", + "modf", + "ceil", + "floor", + "frexp", + "fmod", + "min", + "max", + "fmax", + "fmin", + "abs", + "fabs", + "smooth_abs", + "smooth_max", + "smooth_min", + "tan", + "atan", + "tanh", + "atan2", + "atanh", + "cos", + "acos", + "cosh", + "acosh", + "sin", + "asin", + "sinh", + "asinh", + "cbrt", + "erf", + "erfc", + "nextafter", + "remainder", + "degrees", + "radians", + "copysign", + "trunc", + "hypot", + "dist", + "pi", + "e", + "tau", + "inf", + "nan", + "isclose", + "isfinite", + "isinf", + "isnan", +] + +import xad_autodiff as xad +import math as _math + + +def hypot(*inputs: List[Union["xad.adj_1st.Real", "xad.fwd_1st.Real", float, int]]): + return sqrt(sum(pow(x, 2) for x in inputs)) + + +def dist(p: Union["xad.adj_1st.Real", "xad.fwd_1st.Real", float, int], q): + return sqrt(sum(pow(px - qx, 2) for px, qx in zip(p, q))) + + +def isclose(a, b, *args, **kwargs): + return _math.isclose(xad.value(a), xad.value(b), *args, **kwargs) + + +def isfinite(x): + return _math.isfinite(xad.value(x)) + + +def isinf(x): + return _math.isinf(xad.value(x)) + + +def isnan(x): + return _math.isnan(xad.value(x)) + + +# constants +pi = _math.pi +e = _math.e +tau = _math.tau +inf = _math.inf +nan = _math.nan diff --git a/cmake/SetupOptions.cmake b/cmake/SetupOptions.cmake index 2625b7a7..55614218 100644 --- a/cmake/SetupOptions.cmake +++ b/cmake/SetupOptions.cmake @@ -60,3 +60,5 @@ else() endif() option(XAD_ALLOW_INT_CONVERSION "Add real->int conversion operator, potentially missing to track dependencies" ON) +# Bindings +option(XAD_ENABLE_PYTHON "Enable building the XAD Python module" OFF) \ No newline at end of file diff --git a/cmake/SetupPython.cmake b/cmake/SetupPython.cmake new file mode 100644 index 00000000..53577512 --- /dev/null +++ b/cmake/SetupPython.cmake @@ -0,0 +1,46 @@ +############################################################################## +# +# Setup of Python bindings build, downloading pybind on the fly +# +# This file is part of XAD, a comprehensive C++ library for +# automatic differentiation. +# +# Copyright (C) 2010-2024 Xcelerit Computing Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +if(XAD_ENABLE_PYTHON) + + # fetch pybind11 dependency on the fly + include(FetchContent) + + FetchContent_Declare(pybind11 + GIT_REPOSITORY https://github.com/pybind/pybind11.git + GIT_TAG v2.11.1) + FetchContent_GetProperties(pybind11) + if(NOT pybind11_POPULATED) + FetchContent_Populate(pybind11) + add_subdirectory(${pybind11_SOURCE_DIR} ${pybind11_BINARY_DIR}) + endif() + + set_target_properties(pybind11_headers PROPERTIES + FOLDER "bindings/python" + ) + + # Find poetry: https://python-poetry.org/docs/ + find_program(POETRY_EXECUTABLE poetry REQUIRED) + +endif() \ No newline at end of file diff --git a/docs/.plots/sabs.m b/docs/.plots/sabs.m index 2f4afc88..a00affe9 100644 --- a/docs/.plots/sabs.m +++ b/docs/.plots/sabs.m @@ -3,7 +3,7 @@ % Matlab Plot of the sabs function - for generating the figure for the docs % % This file is part of the XAD user manual. -% Copyright (C) 2010-2023 Xcelerit Computing Ltd. +% Copyright (C) 2010-2024 Xcelerit Computing Ltd. % See the file index.rst for copying condition. % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff --git a/docs/.plots/sabs_plot.m b/docs/.plots/sabs_plot.m index f619d0d9..cc15b768 100644 --- a/docs/.plots/sabs_plot.m +++ b/docs/.plots/sabs_plot.m @@ -3,7 +3,7 @@ % Matlab Plot of the sabs function - for generating the figure for the docs % % This file is part of the XAD user manual. -% Copyright (C) 2010-2023 Xcelerit Computing Ltd. +% Copyright (C) 2010-2024 Xcelerit Computing Ltd. % See the file index.rst for copying condition. % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff --git a/docs/.plots/smax.m b/docs/.plots/smax.m index 83a47d88..57f8bfed 100644 --- a/docs/.plots/smax.m +++ b/docs/.plots/smax.m @@ -3,7 +3,7 @@ % Matlab version of the smax function % % This file is part of the XAD user manual. -% Copyright (C) 2010-2023 Xcelerit Computing Ltd. +% Copyright (C) 2010-2024 Xcelerit Computing Ltd. % See the file index.rst for copying condition. % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff --git a/docs/.plots/smax_plot.m b/docs/.plots/smax_plot.m index 13b1a457..b2a754ab 100644 --- a/docs/.plots/smax_plot.m +++ b/docs/.plots/smax_plot.m @@ -3,7 +3,7 @@ % Matlab Plot of the smax function - for generating the figure for the docs % % This file is part of the XAD user manual. -% Copyright (C) 2010-2023 Xcelerit Computing Ltd. +% Copyright (C) 2010-2024 Xcelerit Computing Ltd. % See the file index.rst for copying condition. % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff --git a/docs/.plots/smin.m b/docs/.plots/smin.m index 8d2ab95c..9cde71b4 100644 --- a/docs/.plots/smin.m +++ b/docs/.plots/smin.m @@ -3,7 +3,7 @@ % Matlab smin function - for generating the figure for the docs % % This file is part of the XAD user manual. -% Copyright (C) 2010-2023 Xcelerit Computing Ltd. +% Copyright (C) 2010-2024 Xcelerit Computing Ltd. % See the file index.rst for copying condition. % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff --git a/docs/.plots/sstep.m b/docs/.plots/sstep.m index 6bac176c..748ab5fb 100644 --- a/docs/.plots/sstep.m +++ b/docs/.plots/sstep.m @@ -3,7 +3,7 @@ % Matlab sstep function based on smin and smax % % This file is part of the XAD user manual. -% Copyright (C) 2010-2023 Xcelerit Computing Ltd. +% Copyright (C) 2010-2024 Xcelerit Computing Ltd. % See the file index.rst for copying condition. % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff --git a/docs/.plots/sstep_plot.m b/docs/.plots/sstep_plot.m index a9352cab..3b27ede6 100644 --- a/docs/.plots/sstep_plot.m +++ b/docs/.plots/sstep_plot.m @@ -3,7 +3,7 @@ % Matlab Plot of the sstep function - for generating the figure for the docs % % This file is part of the XAD user manual. -% Copyright (C) 2010-2023 Xcelerit Computing Ltd. +% Copyright (C) 2010-2024 Xcelerit Computing Ltd. % See the file index.rst for copying condition. % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff --git a/docs/python.md b/docs/python.md new file mode 100644 index 00000000..5cc23877 --- /dev/null +++ b/docs/python.md @@ -0,0 +1,91 @@ +--- +description: > + Python bindings for the XAD automatic differentiation tool. +hide: + - navigation +--- + +# Python Bindings + +The [Python bindings for XAD](https://pypi.org/project/xad-autodiff/) are available on PyPi for all major platforms and operating systems. +There are published with each XAD release, using the same versioning scheme as the C++ version. + +## Installation + +The XAD Python bindings can be installed as usual using `pip` or any other package manager: + +``` +pip install xad-autodiff +``` + +## Usage + +The following example for first-order adjoint mode illustrates how to use it: + +```python +import xad_autodiff.adj_1st as xadj + + +# set independent variables +x0_ad = xadj.Real(1.0) +x1_ad = xadj.Real(1.5) +x2_ad = xadj.Real(1.3) +x3_ad = xadj.Real(1.2) + +with xadj.Tape() as tape: + # and register them + tape.registerInput(x0_ad) + tape.registerInput(x1_ad) + tape.registerInput(x2_ad) + tape.registerInput(x3_ad) + + # start recording derivatives + tape.newRecording() + + # calculate the output + y = x0_ad + x1_ad - x2_ad * x3_ad + + # register and seed adjoint of output + tape.registerOutput(y) + y.derivative = 1.0 + + # compute all other adjoints + tape.computeAdjoints() + + # output results + print(f"y = {y}") + print(f"first order derivatives:\n") + print(f"dy/dx0 = {x0_ad.derivative}") + print(f"dy/dx1 = {x1_ad.derivative}") + print(f"dy/dx2 = {x2_ad.derivative}") + print(f"dy/dx3 = {x3_ad.derivative}") +``` + +The Python bindings follow largely the same syntax and workflow as in C++. + +### Modules and Naming + +| Module | Description | Contents | +|--------|-------------|---------| +| `xad_autodiff` | The main module, which contain global functions and subpackages | `value`, `derivative` | +| `xad_autodiff.exceptions` | Contains all exceptions, with the same names as described in [Exceptions](ref/exceptions.md) | e.g. `NoTapeException` | +| `xad_autodiff.math` | Mirrors Python's `math` module, with functions for XAD's active types. | e.g. `sin`, `exp` | +| `xad_autodiff.fwd_1st` | Active type for first-order forward mode | `Real` | +| `xad_autodiff.adj_1st` | Active type for first-order adjoint mode as well as the corresponding tape type | `Real`, `Tape` | + +### Differences to C++ + +- Only first order forward mode (module `xad_autodiff.fwd_1st`) and first order adjoint mode are supported (module `xad_autodiff.adj_1st`) +- The active type is called `Real` in all modes +- In adjoint mode, a newly constructed `Tape` object is not automatically activated on construction. It can be activated using `tape.activate()` later, but we recommend using a `with` block as illustrated in the example above. +- The math functions in `xad_autodiff.math` have been designed as a drop-in replacement for the standard Python `math` module. They not only support calls with XAD's active type, but also with regular `float` variables. +- Checkpointing and external function features are not yet supported in Python. +- The `x.getDerivative()` and `x.setDerivative()` methods of active types are also available as the Python property `x.derivative` with both set and get functionality. +- The `x.getValue()` method of active types is also available as the read-only property `x.value` +- Since Python does not allow setting references from function return values, the C++ syntax using the global function `derivative(y) = 1.0` is not possible in Python. Instead, use `y.setDerivative(1.0)` or the property setter `y.derivative = 1.0`. +- Complex numbers are not yet supported in the Python bindings. + +## Examples + +Please see the `bindings/python/samples` folder as a starting point to illustrate how to use the Python bindings. + diff --git a/docs/ref/areal.md b/docs/ref/areal.md index 994a7e49..7a01c2ed 100644 --- a/docs/ref/areal.md +++ b/docs/ref/areal.md @@ -124,6 +124,10 @@ This can be used to assign a value to the adjoint, as `#!c++ x.derivative() = 1. which is equivalent to `setDerivative`. It can also be used as a replacement for `getDerivative`. +#### `getSlot` + +`#!c++ slot_type getSlot() const` returns the slot of the variable on the tape. + #### `shouldRecord` `#!c++ bool shouldRecord() const` checks if the variable has been registered with a tape and should therefore diff --git a/mkdocs.yml b/mkdocs.yml index d3287d8f..ee589252 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -169,4 +169,5 @@ nav: - CheckpointCallback: ref/chkpt_cb.md - Exceptions: ref/exceptions.md - Version Information: ref/version.md + - Python: python.md - About: about.md \ No newline at end of file diff --git a/samples/SwapPricer/SwapPricer.hpp b/samples/SwapPricer/SwapPricer.hpp index e83910fe..422e2331 100644 --- a/samples/SwapPricer/SwapPricer.hpp +++ b/samples/SwapPricer/SwapPricer.hpp @@ -33,11 +33,15 @@ T priceSwap(const T* discRates, bool isFixedPay, int n, const double* mat, const using std::pow; T Bfix = 0.0; - for (int t = 0; t < n; ++t) Bfix += fixedRate / pow(1.0 + discRates[t], mat[t]); + for (int t = 0; t < n; ++t) { + Bfix += faceValue * fixedRate / pow(1.0 + discRates[t], mat[t]); + } Bfix += faceValue / pow(1.0 + discRates[n - 1], mat[n - 1]); T Bflt = 0.0; - for (int t = 0; t < n; ++t) Bflt += floatRates[t] / pow(1.0 + discRates[t], mat[t]); + for (int t = 0; t < n; ++t) { + Bflt += faceValue * floatRates[t] / pow(1.0 + discRates[t], mat[t]); + } Bflt += faceValue / pow(1.0 + discRates[n - 1], mat[n - 1]); return isFixedPay ? Bflt - Bfix : Bfix - Bflt; diff --git a/samples/SwapPricer/main.cpp b/samples/SwapPricer/main.cpp index 0dedd41a..9397a93b 100644 --- a/samples/SwapPricer/main.cpp +++ b/samples/SwapPricer/main.cpp @@ -74,7 +74,7 @@ int main() // output results std::cout << "v = " << value(v) << "\n"; - std::cout << "Discount rate sensitivities:\n"; + std::cout << "Discount rate sensitivities for 1 basispoint shift:\n"; for (unsigned i = 0; i < unsigned(nRates); ++i) - std::cout << "dv/dr" << i << " = " << derivative(discRates_ad[i]) << "\n"; + std::cout << "dv/dr" << i << " = " << derivative(discRates_ad[i]) * 0.0001 << "\n"; } diff --git a/src/XAD/StdCompatibility.hpp b/src/XAD/StdCompatibility.hpp index d40b42f1..65bd0143 100644 --- a/src/XAD/StdCompatibility.hpp +++ b/src/XAD/StdCompatibility.hpp @@ -159,6 +159,14 @@ struct is_arithmetic> : std::is_arithmetic { }; template +struct is_signed> : std::is_signed +{ +}; +template +struct is_signed> : std::is_signed +{ +}; +template struct is_pod> : std::false_type { };