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