From 4c8f205c56e9986880afb634a7d33a7f7ca3c950 Mon Sep 17 00:00:00 2001
From: Auto Differentiation Dev Team
<107129969+auto-differentiation-dev@users.noreply.github.com>
Date: Fri, 22 Mar 2024 18:03:22 +0000
Subject: [PATCH 01/11] Adds python bindings to XAD
---
.github/workflows/ci.yml | 2 +
.github/workflows/wheels.yml | 69 ++
.gitignore | 3 +-
CMakeLists.txt | 13 +-
bindings/python/.gitignore | 54 ++
bindings/python/CMakeLists.txt | 35 +
bindings/python/README.md | 13 +
bindings/python/build.py | 96 +++
bindings/python/poetry.lock | 335 +++++++++
bindings/python/pyproject.toml | 106 +++
bindings/python/samples/CMakeLists.txt | 33 +
bindings/python/samples/adj_1st.py | 71 ++
bindings/python/samples/fwd_1st.py | 56 ++
bindings/python/samples/swap_pricer.py | 91 +++
bindings/python/src/CMakeLists.txt | 70 ++
bindings/python/src/exceptions.hpp | 53 ++
bindings/python/src/math.hpp | 234 +++++++
bindings/python/src/module.cpp | 53 ++
bindings/python/src/real.hpp | 193 ++++++
bindings/python/src/tape.hpp | 108 +++
bindings/python/tests/CMakeLists.txt | 35 +
bindings/python/tests/__init__.py | 0
bindings/python/tests/test_exceptions.py | 73 ++
.../tests/test_math_functions_derivatives.py | 502 ++++++++++++++
bindings/python/tests/test_real_operations.py | 645 ++++++++++++++++++
bindings/python/tests/test_tape.py | 149 ++++
bindings/python/xad_autodiff/__init__.py | 63 ++
.../python/xad_autodiff/adj_1st/__init__.py | 84 +++
.../xad_autodiff/exceptions/__init__.py | 39 ++
.../python/xad_autodiff/fwd_1st/__init__.py | 67 ++
bindings/python/xad_autodiff/math/__init__.py | 171 +++++
cmake/SetupOptions.cmake | 2 +
cmake/SetupPython.cmake | 46 ++
docs/.plots/sabs.m | 2 +-
docs/.plots/sabs_plot.m | 2 +-
docs/.plots/smax.m | 2 +-
docs/.plots/smax_plot.m | 2 +-
docs/.plots/smin.m | 2 +-
docs/.plots/sstep.m | 2 +-
docs/.plots/sstep_plot.m | 2 +-
docs/python.md | 91 +++
docs/ref/areal.md | 4 +
mkdocs.yml | 1 +
samples/SwapPricer/SwapPricer.hpp | 8 +-
samples/SwapPricer/main.cpp | 4 +-
src/XAD/StdCompatibility.hpp | 8 +
46 files changed, 3681 insertions(+), 13 deletions(-)
create mode 100644 .github/workflows/wheels.yml
create mode 100644 bindings/python/.gitignore
create mode 100644 bindings/python/CMakeLists.txt
create mode 100644 bindings/python/README.md
create mode 100644 bindings/python/build.py
create mode 100644 bindings/python/poetry.lock
create mode 100644 bindings/python/pyproject.toml
create mode 100644 bindings/python/samples/CMakeLists.txt
create mode 100644 bindings/python/samples/adj_1st.py
create mode 100644 bindings/python/samples/fwd_1st.py
create mode 100644 bindings/python/samples/swap_pricer.py
create mode 100644 bindings/python/src/CMakeLists.txt
create mode 100644 bindings/python/src/exceptions.hpp
create mode 100644 bindings/python/src/math.hpp
create mode 100644 bindings/python/src/module.cpp
create mode 100644 bindings/python/src/real.hpp
create mode 100644 bindings/python/src/tape.hpp
create mode 100644 bindings/python/tests/CMakeLists.txt
create mode 100644 bindings/python/tests/__init__.py
create mode 100644 bindings/python/tests/test_exceptions.py
create mode 100644 bindings/python/tests/test_math_functions_derivatives.py
create mode 100644 bindings/python/tests/test_real_operations.py
create mode 100644 bindings/python/tests/test_tape.py
create mode 100644 bindings/python/xad_autodiff/__init__.py
create mode 100644 bindings/python/xad_autodiff/adj_1st/__init__.py
create mode 100644 bindings/python/xad_autodiff/exceptions/__init__.py
create mode 100644 bindings/python/xad_autodiff/fwd_1st/__init__.py
create mode 100644 bindings/python/xad_autodiff/math/__init__.py
create mode 100644 cmake/SetupPython.cmake
create mode 100644 docs/python.md
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..6f626a29
--- /dev/null
+++ b/.github/workflows/wheels.yml
@@ -0,0 +1,69 @@
+name: Python Wheels
+on: [workflow_dispatch, push, pull_request]
+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
+
+ upload_all:
+ needs: build_wheels
+ 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
+ with:
+ verbose: true
+ repository-url: https://test.pypi.org/legacy/
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/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..75c069f2
--- /dev/null
+++ b/bindings/python/README.md
@@ -0,0 +1,13 @@
+[![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 cutting-edge library designed for automatic differentiation, tailored for both novices and experts. This library excels in production environments, offering unparalleled performance with an emphasis on ease of use. [Automatic differentiation](https://auto-differentiation.github.io/aad/), a critical technique for computing derivatives within computer programmes, is made efficient and straightforward with XAD. Whether you're performing simple arithmetic or complex mathematical functions, XAD ensures accurate and automatic derivative calculations.
+
+Python developers will find the Python bindings for XAD incredibly useful, featuring:
+
+- Support for both forward and adjoint modes at the first order.
+- Robust exception-safety guarantees.
+- Unmatched performance, proven in extensive production deployments.
+- Discover how XAD can revolutionise your computational tasks. Dive into our comprehensive documentation and start integrating XAD into your projects today: 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..a75549ea
--- /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..f3213296
--- /dev/null
+++ b/bindings/python/src/CMakeLists.txt
@@ -0,0 +1,70 @@
+##############################################################################
+#
+# 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
+)
+
+target_link_libraries(_xad_autodiff PRIVATE XAD::xad)
+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..223ccf43
--- /dev/null
+++ b/bindings/python/src/real.hpp
@@ -0,0 +1,193 @@
+/*******************************************************************************
+
+ 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::setAdjoint, "set adjoint of this variable");
+ c.def("shouldRecord", &AReal::shouldRecord,
+ "Check if the variable is registered on tape and should record");
+ c.def("getSlot", &AReal::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", "active arithmetic type for first order adjoint mode")
+ .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);
+
+ add_extra_methods(c);
+
+ c.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", py::overload_cast<>(&T::getValue, py::const_), "get the underlying value")
+ .def("setDerivative", py::overload_cast(&T::setDerivative),
+ "set the adjoint of this variable")
+ .def("getDerivative", py::overload_cast<>(&T::getDerivative, py::const_),
+ "get the adjoint of this variable");
+ c.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");
+ // properties
+ c.def_property_readonly("value", py::overload_cast<>(&T::getValue, py::const_),
+ "read-only property to get the value");
+ c.def_property("derivative", py::overload_cast<>(&T::getDerivative, py::const_),
+ py::overload_cast(&T::setDerivative),
+ "read-write property to get/set derivatives");
+}
diff --git a/bindings/python/src/tape.hpp b/bindings/python/src/tape.hpp
new file mode 100644
index 00000000..f9474a7a
--- /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")
+ .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..debbb8d9
--- /dev/null
+++ b/bindings/python/tests/test_exceptions.py
@@ -0,0 +1,73 @@
+##############################################################################
+#
+# 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.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..4f449613
--- /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 type(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..35d66b23
--- /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) as e:
+ 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..1292cf8e
--- /dev/null
+++ b/bindings/python/xad_autodiff/__init__.py
@@ -0,0 +1,63 @@
+##############################################################################
+#
+# 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 . import adj_1st
+from . import fwd_1st
+from . import math
+from . import exceptions
+
+__all__ = ["value", "derivative", "math", "adj_1st", "fwd_1st", "exceptions"]
+
+
+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..0f70f9e8
--- /dev/null
+++ b/bindings/python/xad_autodiff/adj_1st/__init__.py
@@ -0,0 +1,84 @@
+##############################################################################
+#
+# 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.adj_1st import Real, Tape
+
+__all__ = ["Real", "Tape"]
+
+
+def _register_inputs(self, inputs):
+ for input in inputs:
+ self.registerInput(input)
+
+
+Tape.registerInputs = _register_inputs
+
+
+def _register_outputs(self, outputs):
+ for output in outputs:
+ self.registerOutput(output)
+
+
+Tape.registerOutputs = _register_outputs
+
+
+# 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/exceptions/__init__.py b/bindings/python/xad_autodiff/exceptions/__init__.py
new file mode 100644
index 00000000..1f87ae48
--- /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.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..fb95a6c2
--- /dev/null
+++ b/bindings/python/xad_autodiff/fwd_1st/__init__.py
@@ -0,0 +1,67 @@
+##############################################################################
+#
+# 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.fwd_1st import Real
+
+__all__ = ["Real"]
+
+
+# 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..71d1e3e6
--- /dev/null
+++ b/bindings/python/xad_autodiff/math/__init__.py
@@ -0,0 +1,171 @@
+##############################################################################
+#
+# 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.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
\ No newline at end of file
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
{
};
From c650e2b84d3877f5c4014a1579d4f791fa209b4f Mon Sep 17 00:00:00 2001
From: Auto Differentiation Dev Team
<107129969+auto-differentiation-dev@users.noreply.github.com>
Date: Fri, 22 Mar 2024 18:23:26 +0000
Subject: [PATCH 02/11] Python module includes Tape.cpp as source, to
synchronise compilation flags
---
CHANGELOG.md | 1 +
bindings/python/src/CMakeLists.txt | 6 +++++-
2 files changed, 6 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 48391e03..db85f148 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
+- Python bindings as [xad-autodiff](https://pypi.org/project/xad-autodiff/)
- Support for enhanced debugger visualisations in Visual Studio (@dholden3)
### Changed
diff --git a/bindings/python/src/CMakeLists.txt b/bindings/python/src/CMakeLists.txt
index f3213296..bade4a3a 100644
--- a/bindings/python/src/CMakeLists.txt
+++ b/bindings/python/src/CMakeLists.txt
@@ -24,9 +24,13 @@
pybind11_add_module(_xad_autodiff MODULE
module.cpp math.hpp tape.hpp real.hpp exceptions.hpp
+ ${PROJECT_SOURCE_DIR}/src/Tape.cpp
)
-target_link_libraries(_xad_autodiff PRIVATE XAD::xad)
+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
From 8db74bed219e6c88adca41f2623c96522c09f582 Mon Sep 17 00:00:00 2001
From: Auto Differentiation Dev Team
<107129969+auto-differentiation-dev@users.noreply.github.com>
Date: Sat, 23 Mar 2024 07:16:10 +0000
Subject: [PATCH 03/11] Modifies get/set Derivative methods in attempt to solve
Mac Python import error
---
bindings/python/src/real.hpp | 27 ++++++++++++++++-----------
1 file changed, 16 insertions(+), 11 deletions(-)
diff --git a/bindings/python/src/real.hpp b/bindings/python/src/real.hpp
index 223ccf43..970ce8fd 100644
--- a/bindings/python/src/real.hpp
+++ b/bindings/python/src/real.hpp
@@ -56,7 +56,7 @@ inline T py_fmod(const T1& x, const T2& y)
}
template
-inline std::pair py_divmod(const T1& x, const T2& y)
+inline std::pair py_divmod(const T1& x, const T2& y)
{
T mod = py_fmod(x, y);
T div = (x - mod) / y;
@@ -173,11 +173,14 @@ void py_real(py::module_& m)
.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", py::overload_cast<>(&T::getValue, py::const_), "get the underlying value")
- .def("setDerivative", py::overload_cast(&T::setDerivative),
- "set the adjoint of this variable")
- .def("getDerivative", py::overload_cast<>(&T::getDerivative, py::const_),
- "get the adjoint of this variable");
+ .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");
c.def(
"conjugate", [](const T& x) { return x; }, "complex conjugate")
.def(
@@ -185,9 +188,11 @@ void py_real(py::module_& m)
.def(
"imag", [](const T&) { return T(0.0); }, "imaginary part");
// properties
- c.def_property_readonly("value", py::overload_cast<>(&T::getValue, py::const_),
- "read-only property to get the value");
- c.def_property("derivative", py::overload_cast<>(&T::getDerivative, py::const_),
- py::overload_cast(&T::setDerivative),
- "read-write property to get/set derivatives");
+ c.def_property_readonly(
+ "value", [](const T& self) { return self.getValue(); },
+ "read-only property to get the value");
+ c.def_property(
+ "derivative", [](const T& self) { return self.getDerivative(); },
+ [](T& self, double v) { self.setDerivative(v); },
+ "read-write property to get/set derivatives");
}
From a2ad3a2954cdc51dd40a8802bd25cbe2ba3deca8 Mon Sep 17 00:00:00 2001
From: Auto Differentiation Dev Team
<107129969+auto-differentiation-dev@users.noreply.github.com>
Date: Sat, 23 Mar 2024 07:41:24 +0000
Subject: [PATCH 04/11] Fixing relative imports
---
bindings/python/xad_autodiff/__init__.py | 8 +++-----
bindings/python/xad_autodiff/adj_1st/__init__.py | 2 +-
bindings/python/xad_autodiff/exceptions/__init__.py | 2 +-
bindings/python/xad_autodiff/fwd_1st/__init__.py | 2 +-
bindings/python/xad_autodiff/math/__init__.py | 2 +-
5 files changed, 7 insertions(+), 9 deletions(-)
diff --git a/bindings/python/xad_autodiff/__init__.py b/bindings/python/xad_autodiff/__init__.py
index 1292cf8e..8b94a58e 100644
--- a/bindings/python/xad_autodiff/__init__.py
+++ b/bindings/python/xad_autodiff/__init__.py
@@ -25,12 +25,10 @@
"""Python bindings for the XAD comprehensive library for automatic differentiation"""
from typing import Any, Union
-from . import adj_1st
-from . import fwd_1st
-from . import math
-from . import exceptions
+from ._xad_autodiff import adj_1st
+from ._xad_autodiff import fwd_1st
-__all__ = ["value", "derivative", "math", "adj_1st", "fwd_1st", "exceptions"]
+__all__ = ["value", "derivative"]
def value(x: Union[adj_1st.Real, fwd_1st.Real, Any]) -> float:
diff --git a/bindings/python/xad_autodiff/adj_1st/__init__.py b/bindings/python/xad_autodiff/adj_1st/__init__.py
index 0f70f9e8..5c8be16d 100644
--- a/bindings/python/xad_autodiff/adj_1st/__init__.py
+++ b/bindings/python/xad_autodiff/adj_1st/__init__.py
@@ -23,7 +23,7 @@
##############################################################################
from typing import Tuple, Type
-from .._xad_autodiff.adj_1st import Real, Tape
+from xad_autodiff._xad_autodiff.adj_1st import Real, Tape
__all__ = ["Real", "Tape"]
diff --git a/bindings/python/xad_autodiff/exceptions/__init__.py b/bindings/python/xad_autodiff/exceptions/__init__.py
index 1f87ae48..d3ca7286 100644
--- a/bindings/python/xad_autodiff/exceptions/__init__.py
+++ b/bindings/python/xad_autodiff/exceptions/__init__.py
@@ -22,7 +22,7 @@
#
##############################################################################
-from .._xad_autodiff.exceptions import (
+from xad_autodiff._xad_autodiff.exceptions import (
XadException,
TapeAlreadyActive,
OutOfRange,
diff --git a/bindings/python/xad_autodiff/fwd_1st/__init__.py b/bindings/python/xad_autodiff/fwd_1st/__init__.py
index fb95a6c2..1bf51c83 100644
--- a/bindings/python/xad_autodiff/fwd_1st/__init__.py
+++ b/bindings/python/xad_autodiff/fwd_1st/__init__.py
@@ -23,7 +23,7 @@
##############################################################################
from typing import Tuple, Type
-from .._xad_autodiff.fwd_1st import Real
+from xad_autodiff._xad_autodiff.fwd_1st import Real
__all__ = ["Real"]
diff --git a/bindings/python/xad_autodiff/math/__init__.py b/bindings/python/xad_autodiff/math/__init__.py
index 71d1e3e6..0d168326 100644
--- a/bindings/python/xad_autodiff/math/__init__.py
+++ b/bindings/python/xad_autodiff/math/__init__.py
@@ -29,7 +29,7 @@
"""
from typing import Union, List
-from .._xad_autodiff.math import (
+from xad_autodiff._xad_autodiff.math import (
sqrt,
pow,
log10,
From 62cadb6b30efbaaf391f5b7314915125646573e4 Mon Sep 17 00:00:00 2001
From: Auto Differentiation Dev Team
<107129969+auto-differentiation-dev@users.noreply.github.com>
Date: Sat, 23 Mar 2024 07:50:26 +0000
Subject: [PATCH 05/11] Makes classes support dynamic attributes
---
bindings/python/src/real.hpp | 2 +-
bindings/python/src/tape.hpp | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/bindings/python/src/real.hpp b/bindings/python/src/real.hpp
index 970ce8fd..87d5996c 100644
--- a/bindings/python/src/real.hpp
+++ b/bindings/python/src/real.hpp
@@ -72,7 +72,7 @@ inline T py_floordiv(const T1& x, const T2& y)
template
void py_real(py::module_& m)
{
- auto& c = py::class_(m, "Real", "active arithmetic type for first order adjoint mode")
+ auto& c = py::class_(m, "Real", py::dynamic_attr(), "active arithmetic type for first order adjoint mode")
.def(py::init())
.def(py::init<>())
.def(py::self == py::self)
diff --git a/bindings/python/src/tape.hpp b/bindings/python/src/tape.hpp
index f9474a7a..a8e34def 100644
--- a/bindings/python/src/tape.hpp
+++ b/bindings/python/src/tape.hpp
@@ -38,7 +38,7 @@ using AReal = mode::active_type;
void py_tape(py::module_ &m)
{
- py::class_(m, "Tape")
+ py::class_(m, "Tape", py::dynamic_attr())
.def(py::init([] { return std::make_unique(false); }),
"constructs a tape without activating it")
.def(
From a5c06af5a438a5b37a1400a4fc9d42bf5dba4c63 Mon Sep 17 00:00:00 2001
From: Auto Differentiation Dev Team
<107129969+auto-differentiation-dev@users.noreply.github.com>
Date: Sat, 23 Mar 2024 08:11:26 +0000
Subject: [PATCH 06/11] Add properties for value and derivative in Python
---
bindings/python/src/real.hpp | 46 +++++++++----------
bindings/python/xad_autodiff/__init__.py | 3 +-
.../python/xad_autodiff/adj_1st/__init__.py | 25 +++++++++-
.../python/xad_autodiff/fwd_1st/__init__.py | 9 ++++
4 files changed, 55 insertions(+), 28 deletions(-)
diff --git a/bindings/python/src/real.hpp b/bindings/python/src/real.hpp
index 87d5996c..83642149 100644
--- a/bindings/python/src/real.hpp
+++ b/bindings/python/src/real.hpp
@@ -36,10 +36,15 @@ using FReal = xad::FReal;
inline void add_extra_methods(py::class_& c)
{
- c.def("setAdjoint", &AReal::setAdjoint, "set adjoint of this variable");
- c.def("shouldRecord", &AReal::shouldRecord,
- "Check if the variable is registered on tape and should record");
- c.def("getSlot", &AReal::getSlot, "Get the slot of this variable on the tape");
+ 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_&) {}
@@ -72,19 +77,20 @@ inline T py_floordiv(const T1& x, const T2& 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")
+ auto& c = py::class_(m, "Real", py::dynamic_attr(),
+ "active arithmetic type for first order adjoint mode")
.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(py::init<>());
add_extra_methods(c);
- c.def("__int__", [](const T& d) { return int(d.getValue()); })
+ c.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; })
@@ -180,19 +186,11 @@ void py_real(py::module_& m)
"set the adjoint of this variable")
.def(
"getDerivative", [](const T& self) { return self.getDerivative(); },
- "get the adjoint of this variable");
- c.def(
- "conjugate", [](const T& x) { return x; }, "complex conjugate")
+ "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");
- // properties
- c.def_property_readonly(
- "value", [](const T& self) { return self.getValue(); },
- "read-only property to get the value");
- c.def_property(
- "derivative", [](const T& self) { return self.getDerivative(); },
- [](T& self, double v) { self.setDerivative(v); },
- "read-write property to get/set derivatives");
}
diff --git a/bindings/python/xad_autodiff/__init__.py b/bindings/python/xad_autodiff/__init__.py
index 8b94a58e..d79ebd43 100644
--- a/bindings/python/xad_autodiff/__init__.py
+++ b/bindings/python/xad_autodiff/__init__.py
@@ -25,8 +25,7 @@
"""Python bindings for the XAD comprehensive library for automatic differentiation"""
from typing import Any, Union
-from ._xad_autodiff import adj_1st
-from ._xad_autodiff import fwd_1st
+from ._xad_autodiff import adj_1st, fwd_1st
__all__ = ["value", "derivative"]
diff --git a/bindings/python/xad_autodiff/adj_1st/__init__.py b/bindings/python/xad_autodiff/adj_1st/__init__.py
index 5c8be16d..13db306a 100644
--- a/bindings/python/xad_autodiff/adj_1st/__init__.py
+++ b/bindings/python/xad_autodiff/adj_1st/__init__.py
@@ -43,12 +43,22 @@ def _register_outputs(self, outputs):
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
@@ -56,29 +66,40 @@ 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, )
+ 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
+
+Real.__format__ = _format
diff --git a/bindings/python/xad_autodiff/fwd_1st/__init__.py b/bindings/python/xad_autodiff/fwd_1st/__init__.py
index 1bf51c83..3e14c12b 100644
--- a/bindings/python/xad_autodiff/fwd_1st/__init__.py
+++ b/bindings/python/xad_autodiff/fwd_1st/__init__.py
@@ -28,6 +28,15 @@
__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"""
From d2fcc5b7eb1a18dce2b1fdec01f252435c7d4dc2 Mon Sep 17 00:00:00 2001
From: Auto Differentiation Dev Team
<107129969+auto-differentiation-dev@users.noreply.github.com>
Date: Sat, 23 Mar 2024 08:18:37 +0000
Subject: [PATCH 07/11] Fixes reference to py::class - should be a value
---
bindings/python/src/real.hpp | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/bindings/python/src/real.hpp b/bindings/python/src/real.hpp
index 83642149..72a0dc30 100644
--- a/bindings/python/src/real.hpp
+++ b/bindings/python/src/real.hpp
@@ -77,14 +77,12 @@ inline T py_floordiv(const T1& x, const T2& 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")
- .def(py::init())
- .def(py::init<>());
+ auto c = py::class_(m, "Real", py::dynamic_attr(),
+ "active arithmetic type for first order adjoint mode");
- add_extra_methods(c);
-
- c.def(py::self == py::self)
+ 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)
@@ -193,4 +191,6 @@ void py_real(py::module_& m)
"real", [](const T& x) { return x; }, "real part")
.def(
"imag", [](const T&) { return T(0.0); }, "imaginary part");
+
+ add_extra_methods(c);
}
From 9f242a72635a1e2e92534b307ca6c2c65abfb3a8 Mon Sep 17 00:00:00 2001
From: Auto Differentiation Dev Team
<107129969+auto-differentiation-dev@users.noreply.github.com>
Date: Sat, 23 Mar 2024 08:36:28 +0000
Subject: [PATCH 08/11] Documentation update
---
bindings/python/README.md | 53 +++++++++++++++++++++++++++++++++-
bindings/python/pyproject.toml | 6 ++--
2 files changed, 55 insertions(+), 4 deletions(-)
diff --git a/bindings/python/README.md b/bindings/python/README.md
index 75c069f2..a9e3ea39 100644
--- a/bindings/python/README.md
+++ b/bindings/python/README.md
@@ -9,5 +9,56 @@ Python developers will find the Python bindings for XAD incredibly useful, featu
- Support for both forward and adjoint modes at the first order.
- Robust exception-safety guarantees.
- Unmatched performance, proven in extensive production deployments.
-- Discover how XAD can revolutionise your computational tasks. Dive into our comprehensive documentation and start integrating XAD into your projects today: https://auto-differentiation.github.io/python.
+- Discover how XAD can revolutionise your computational tasks. Dive into our comprehensive [documentation](https://auto-differentiation.github.io/python) and start integrating XAD into your projects today.
+## Getting Started
+
+Install:
+
+```
+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/pyproject.toml b/bindings/python/pyproject.toml
index a75549ea..0eeaa161 100644
--- a/bindings/python/pyproject.toml
+++ b/bindings/python/pyproject.toml
@@ -70,9 +70,9 @@ 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"
+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"
From 33ac5ab8081a4f9ab943c5c99f5697abfbfc41b7 Mon Sep 17 00:00:00 2001
From: Auto Differentiation Dev Team
<107129969+auto-differentiation-dev@users.noreply.github.com>
Date: Sat, 23 Mar 2024 09:17:16 +0000
Subject: [PATCH 09/11] Updated full wheel build, uploading to pypi on tagged
releases
---
.github/workflows/wheels.yml | 60 ++++++++++++++++++++++++++++++++++--
1 file changed, 58 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml
index 6f626a29..8b458265 100644
--- a/.github/workflows/wheels.yml
+++ b/.github/workflows/wheels.yml
@@ -1,5 +1,33 @@
+# 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: [workflow_dispatch, push, pull_request]
+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
@@ -49,8 +77,12 @@ jobs:
path: ./wheelhouse/*.whl
if-no-files-found: error
- upload_all:
+ 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
@@ -64,6 +96,30 @@ jobs:
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
+
From 8e830a36fede710d407cfc55c87a6dff0c11b504 Mon Sep 17 00:00:00 2001
From: Auto Differentiation Dev Team
<107129969+auto-differentiation-dev@users.noreply.github.com>
Date: Sat, 23 Mar 2024 12:30:40 +0000
Subject: [PATCH 10/11] Update description of Python package
---
bindings/python/README.md | 30 ++++++++++++++++++++++++------
1 file changed, 24 insertions(+), 6 deletions(-)
diff --git a/bindings/python/README.md b/bindings/python/README.md
index a9e3ea39..345f744d 100644
--- a/bindings/python/README.md
+++ b/bindings/python/README.md
@@ -2,14 +2,32 @@
[![PyPI version](https://badge.fury.io/py/xad-autodiff.svg)](https://pypi.org/project/xad-autodiff/)
-XAD is a cutting-edge library designed for automatic differentiation, tailored for both novices and experts. This library excels in production environments, offering unparalleled performance with an emphasis on ease of use. [Automatic differentiation](https://auto-differentiation.github.io/aad/), a critical technique for computing derivatives within computer programmes, is made efficient and straightforward with XAD. Whether you're performing simple arithmetic or complex mathematical functions, XAD ensures accurate and automatic derivative calculations.
+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.
-Python developers will find the Python bindings for XAD incredibly useful, featuring:
+The Python bindings for XAD offer the following features:
-- Support for both forward and adjoint modes at the first order.
-- Robust exception-safety guarantees.
-- Unmatched performance, proven in extensive production deployments.
-- Discover how XAD can revolutionise your computational tasks. Dive into our comprehensive [documentation](https://auto-differentiation.github.io/python) and start integrating XAD into your projects today.
+- 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
From 4dd6513323665b68cef815b20c44d51d721ada55 Mon Sep 17 00:00:00 2001
From: Auto Differentiation Dev Team
<107129969+auto-differentiation-dev@users.noreply.github.com>
Date: Sun, 24 Mar 2024 07:11:24 +0000
Subject: [PATCH 11/11] Cleanup of docs, fixing Codacy issues
---
CHANGELOG.md | 19 ++++++++++---------
bindings/python/README.md | 18 +++++++++++++-----
bindings/python/tests/test_exceptions.py | 1 +
bindings/python/tests/test_real_operations.py | 2 +-
bindings/python/tests/test_tape.py | 2 +-
.../python/xad_autodiff/adj_1st/__init__.py | 8 ++++----
bindings/python/xad_autodiff/math/__init__.py | 10 +++++++---
7 files changed, 37 insertions(+), 23 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index db85f148..835ea140 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,16 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
-- Python bindings as [xad-autodiff](https://pypi.org/project/xad-autodiff/)
-- 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
@@ -78,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/bindings/python/README.md b/bindings/python/README.md
index 345f744d..be3384be 100644
--- a/bindings/python/README.md
+++ b/bindings/python/README.md
@@ -2,7 +2,14 @@
[![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.
+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:
@@ -10,16 +17,17 @@ The Python bindings for XAD offer the following features:
- 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).
+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 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
+- **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
@@ -33,7 +41,7 @@ Automatic differentiation has many application areas, for example:
Install:
-```
+```text
pip install xad-autodiff
```
diff --git a/bindings/python/tests/test_exceptions.py b/bindings/python/tests/test_exceptions.py
index debbb8d9..4134470f 100644
--- a/bindings/python/tests/test_exceptions.py
+++ b/bindings/python/tests/test_exceptions.py
@@ -61,6 +61,7 @@ def test_exceptions_adjoints_not_initialized(exception):
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)
diff --git a/bindings/python/tests/test_real_operations.py b/bindings/python/tests/test_real_operations.py
index 4f449613..5437c19f 100644
--- a/bindings/python/tests/test_real_operations.py
+++ b/bindings/python/tests/test_real_operations.py
@@ -327,7 +327,7 @@ def test_truncation_funcs(ad_type, func):
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 type(func(ad_type(1.1))) == int
+ assert isinstance(func(ad_type(1.1)), int)
@pytest.mark.parametrize("ad_type", [AReal, FReal], ids=["adj", "fwd"])
diff --git a/bindings/python/tests/test_tape.py b/bindings/python/tests/test_tape.py
index 35d66b23..873cb2a7 100644
--- a/bindings/python/tests/test_tape.py
+++ b/bindings/python/tests/test_tape.py
@@ -134,7 +134,7 @@ def test_set_derivative_value():
t.registerInput(x)
t.setDerivative(x, 1.0)
assert t.derivative(x) == 1.0
- with pytest.raises(exceptions.OutOfRange) as e:
+ with pytest.raises(exceptions.OutOfRange):
derivative(t.setDerivative(1231, 0.0))
diff --git a/bindings/python/xad_autodiff/adj_1st/__init__.py b/bindings/python/xad_autodiff/adj_1st/__init__.py
index 13db306a..f8008016 100644
--- a/bindings/python/xad_autodiff/adj_1st/__init__.py
+++ b/bindings/python/xad_autodiff/adj_1st/__init__.py
@@ -29,16 +29,16 @@
def _register_inputs(self, inputs):
- for input in inputs:
- self.registerInput(input)
+ for i in inputs:
+ self.registerInput(i)
Tape.registerInputs = _register_inputs
def _register_outputs(self, outputs):
- for output in outputs:
- self.registerOutput(output)
+ for o in outputs:
+ self.registerOutput(o)
Tape.registerOutputs = _register_outputs
diff --git a/bindings/python/xad_autodiff/math/__init__.py b/bindings/python/xad_autodiff/math/__init__.py
index 0d168326..85b48e90 100644
--- a/bindings/python/xad_autodiff/math/__init__.py
+++ b/bindings/python/xad_autodiff/math/__init__.py
@@ -23,7 +23,7 @@
##############################################################################
"""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
+ 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.
"""
@@ -137,7 +137,6 @@
"isfinite",
"isinf",
"isnan",
-
]
import xad_autodiff as xad
@@ -151,21 +150,26 @@ def hypot(*inputs: List[Union["xad.adj_1st.Real", "xad.fwd_1st.Real", float, int
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
\ No newline at end of file
+nan = _math.nan