Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Weekly grouped updates for Python deps (pyproject.toml) and GitHub Actions.
# Submodules (QuantLib, XAD, QuantLibAAD, pybind11) are bumped manually: XAD and
# pybind11 must match the xad package exactly.
# Submodules (QuantLib, QuantLibAAD, pybind11) are bumped manually: pybind11 must
# match the xad package exactly.
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file

version: 2
Expand All @@ -10,7 +10,7 @@ updates:
schedule:
interval: "weekly"
ignore:
# pinned to the XAD in lib/xad
# one major.minor series, bumped manually
- dependency-name: "xad"
groups:
python:
Expand Down
3 changes: 0 additions & 3 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
[submodule "lib/QuantLib"]
path = lib/QuantLib
url = https://github.com/lballabio/QuantLib.git
[submodule "lib/xad"]
path = lib/xad
url = https://github.com/auto-differentiation/xad.git
[submodule "lib/QuantLibAAD"]
path = lib/QuantLibAAD
url = https://github.com/auto-differentiation/QuantLibAAD.git
Expand Down
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [1.41.0] - Unreleased

- Matches QuantLib and QuantLib Python bindings version 1.41
- Builds against XAD 2.1 and QuantLibAAD 1.41 (formerly QuantLib-Risks-Cpp), requires xad 2.1
- Builds against QuantLibAAD 1.41 (formerly QuantLib-Risks-Cpp) and the shared libxad of the installed xad 2.2 package (`xad>=2.2,<2.3`), which is not bundled
- Tapes are shared with xad through libxad, thread-local per Python thread; QuantLib no longer patches `xad.adj_1st.Tape`
- Builds the unmodified QuantLib 1.41 release, no QuantLib patches
- One-step build with scikit-build-core; wheels for Python 3.10-3.14 on Linux, macOS and Windows
- `tools/sync-swig.sh` syncs `SWIG/` with a QuantLib-SWIG release, re-applying the XAD changes in `SWIG/patches/`
Expand Down
8 changes: 5 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,19 @@ endif()
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
# static MSVC runtime everywhere (QuantLib, XAD, the module), as in xad-py
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
# dynamic MSVC runtime everywhere (QuantLib, the module), as libxad is a DLL
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")

option(QLR_QUANTLIB_ONLY
"Only build and install QuantLib + XAD + QuantLibAAD, for reuse via QLR_QUANTLIB_PREFIX"
"Only build and install QuantLib + QuantLibAAD, for reuse via QLR_QUANTLIB_PREFIX"
OFF)
set(QLR_QUANTLIB_PREFIX "" CACHE PATH
"Install prefix of a QLR_QUANTLIB_ONLY build to reuse (empty: build QuantLib from lib/)")

list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake")

include(QLRXad)

if(QLR_QUANTLIB_PREFIX AND NOT QLR_QUANTLIB_ONLY)
include(PrebuiltQuantLib)
else()
Expand Down
14 changes: 7 additions & 7 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ accepted:

The package is built in one step by [scikit-build-core](https://scikit-build-core.readthedocs.io)
(see `pyproject.toml`): CMake builds QuantLib as a static library together with
[XAD](https://github.com/auto-differentiation/xad) and
[QuantLibAAD](https://github.com/auto-differentiation/QuantLibAAD) from the `lib/` submodules,
then the SWIG module from `SWIG/` and `Python/QuantLib_Risks/`.
against [XAD](https://github.com/auto-differentiation/xad) from the installed
[xad](https://pypi.org/project/xad/) package (`xad.get_cmake_dir()`), then the SWIG module from `SWIG/` and `Python/QuantLib_Risks/`.

Requirements: Python 3.10+, and a C++17 compiler.
CMake, Ninja and SWIG are installed as build dependencies.
Expand All @@ -72,16 +72,16 @@ cd /tmp && python -m pytest --import-mode=importlib /path/to/QuantLib-Risks-Py/P

Notes:

- The binary module shares XAD's tape and `Real` type with the
[xad](https://pypi.org/project/xad/) package, so `lib/xad` and `lib/pybind11` must be the
exact versions xad is built with, and the `xad` requirement is pinned to that XAD
major.minor series. Update them together.
- The binary module links the xad package's shared libxad (found at runtime through the
rpath `../xad/lib`, never bundled in the wheel) and shares its `Real` type, so
`lib/pybind11` must be the exact version xad is built with, and the `xad` requirement is
pinned to one major.minor series. Update them together.
- `lib/QuantLib` is an unmodified QuantLib release; AAD support comes from QuantLibAAD.
- Building QuantLib takes most of the time. To reuse it across builds (e.g. several Python
versions), install it once and point the package build at it:

```bash
cmake -DPREFIX=$PWD/build/quantlib-prefix -P tools/build-quantlib.cmake
cmake -DPREFIX=$PWD/build/quantlib-prefix -DPYTHON=$(which python) -P tools/build-quantlib.cmake
pip install -v . -Ccmake.define.QLR_QUANTLIB_PREFIX=$PWD/build/quantlib-prefix
```

Expand Down
8 changes: 7 additions & 1 deletion Python/QuantLib_Risks/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,19 @@ swig_add_library(QuantLib_Risks
LANGUAGE python
OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}
SOURCES ${QLR_SWIG_SOURCE} converters.cpp)
target_link_libraries(QuantLib_Risks PRIVATE QuantLib::QuantLib pybind11::module)
target_link_libraries(QuantLib_Risks PRIVATE QuantLib::QuantLib XAD::xad pybind11::module)
target_compile_definitions(QuantLib_Risks PRIVATE
QL_XAD=1
QLR_VERSION=\"${QLR_VERSION}\"
QLR_HEX_VERSION=${QLR_HEX_VERSION})
target_include_directories(QuantLib_Risks PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
set_target_properties(QuantLib_Risks PROPERTIES SUFFIX "${PYTHON_MODULE_EXTENSION}")
# libxad from the installed xad package (on Windows, importing xad adds its DLL directory)
if(APPLE)
set_target_properties(QuantLib_Risks PROPERTIES INSTALL_RPATH "@loader_path/../xad/lib")
elseif(UNIX)
set_target_properties(QuantLib_Risks PROPERTIES INSTALL_RPATH "$ORIGIN/../xad/lib")
endif()
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug|RelWithDebInfo")
pybind11_strip(QuantLib_Risks)
endif()
Expand Down
37 changes: 2 additions & 35 deletions Python/QuantLib_Risks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
FOR A PARTICULAR PURPOSE. See the license for more details.
"""

import xad # loads libxad, which the binary module links against

from .QuantLib_Risks import *

if XAD_ENABLED:
Expand All @@ -52,41 +54,6 @@ def PairDoubleVector(
) -> Tuple[List[Real], List[Real]]:
return ([Real(x) for x in x1], [Real(x) for x in x2])

# monkey-patch the tape activation function to register this tape with QuantLib's internal
# global tape pointer (we have 2 due to double static linking)
def _wrap_tape_activation():
original_enter = Tape.__enter__

def _tape_enter(t: Tape):
t = original_enter(t)
_QuantLib_Risks._activate_tape(t)
return t

original_exit = Tape.__exit__

def _tape_exit(t: Tape, *args, **kwargs):
original_exit(t, *args, **kwargs)
_QuantLib_Risks._deactivate_tape()

original_activate = Tape.activate

def _tape_activate(t: Tape):
original_activate(t)
_QuantLib_Risks._activate_tape(t)

original_deactivate = Tape.deactivate

def _tape_deactivate(t: Tape):
original_deactivate(t)
_QuantLib_Risks._deactivate_tape()

Tape.__enter__ = _tape_enter
Tape.__exit__ = _tape_exit
Tape.activate = _tape_activate
Tape.deactivate = _tape_deactivate

_wrap_tape_activation()

if hasattr(_QuantLib_Risks, "__version__"):
__version__ = _QuantLib_Risks.__version__
elif hasattr(_QuantLib_Risks.cvar, "__version__"):
Expand Down
14 changes: 1 addition & 13 deletions SWIG/tape.i
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/******************************************************************************
* Tape activation functions to ensure an xad global tape instance
* matches the one in QuantLib-Risks.
* Conversion of xad.adj_1st.Tape arguments to xad::Tape<double>&.
*
* This file is part of QuantLib-Risks, a Python wrapper for QuantLib enabled
* for risk computation using automatic differentiation. It uses XAD,
Expand Down Expand Up @@ -30,14 +29,6 @@
#include "converters.hpp"
#include <XAD/XAD.hpp>

void _activate_tape(xad::Tape<double>& t) {
xad::Tape<double>::setActive(&t);
}

void _deactivate_tape() {
xad::Tape<double>::deactivateAll();
}

typedef xad::Tape<double> Tape;
%}

Expand All @@ -59,7 +50,4 @@ typedef xad::Tape<double> Tape;
$1 = check_Tape($input) ? 1 : 0;
}

void _activate_tape(xad::Tape<double>& t);
void _deactivate_tape();

#endif
24 changes: 8 additions & 16 deletions cmake/PrebuiltQuantLib.cmake
Original file line number Diff line number Diff line change
@@ -1,22 +1,14 @@
# Uses QuantLib, XAD and QuantLibAAD installed by a QLR_QUANTLIB_ONLY build in
# QLR_QUANTLIB_PREFIX. Defines QuantLib::QuantLib.
# Uses QuantLib and QuantLibAAD installed by a QLR_QUANTLIB_ONLY build in
# QLR_QUANTLIB_PREFIX, with XAD::xad from QLRXad. Defines QuantLib::QuantLib.

file(TO_CMAKE_PATH "${QLR_QUANTLIB_PREFIX}" QLR_QUANTLIB_PREFIX)
list(PREPEND CMAKE_PREFIX_PATH "${QLR_QUANTLIB_PREFIX}")
find_package(QuantLibAAD CONFIG REQUIRED NO_DEFAULT_PATH PATHS "${QLR_QUANTLIB_PREFIX}")

# guard against a stale prefix: XAD must match lib/xad and share the tape with xad-py
file(READ "${PROJECT_SOURCE_DIR}/lib/xad/VERSION" xad_src_version)
string(STRIP "${xad_src_version}" xad_src_version)
get_target_property(xad_includes XAD::xad INTERFACE_INCLUDE_DIRECTORIES)
set(xad_no_tls)
foreach(dir IN LISTS xad_includes)
if(EXISTS "${dir}/XAD/Config.hpp")
file(STRINGS "${dir}/XAD/Config.hpp" xad_no_tls REGEX "^#define XAD_NO_THREADLOCAL")
endif()
endforeach()
if(NOT XAD_VERSION STREQUAL xad_src_version OR NOT xad_no_tls)
message(FATAL_ERROR "XAD in ${QLR_QUANTLIB_PREFIX} (${XAD_VERSION}) does not match "
"lib/xad (${xad_src_version}) with XAD_NO_THREADLOCAL; rebuild it")
# guard against a stale prefix: QuantLib must be built with the XAD found by QLRXad
include("${QuantLibAAD_DIR}/QLRXadVersion.cmake" OPTIONAL)
if(NOT QLR_QUANTLIB_XAD_VERSION STREQUAL XAD_VERSION)
message(FATAL_ERROR "QuantLib in ${QLR_QUANTLIB_PREFIX} was built with XAD "
"'${QLR_QUANTLIB_XAD_VERSION}', not ${XAD_VERSION} (${XAD_DIR}); rebuild it")
endif()
message(STATUS "Using prebuilt QuantLib ${QuantLib_VERSION} and XAD ${XAD_VERSION} from ${QLR_QUANTLIB_PREFIX}")
message(STATUS "Using prebuilt QuantLib ${QuantLib_VERSION} from ${QLR_QUANTLIB_PREFIX}")
45 changes: 45 additions & 0 deletions cmake/QLRXad.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
##############################################################################
#
# This file is part of QuantLib-Risks, a Python wrapper for QuantLib enabled
# for risk computation using automatic differentiation. It uses XAD,
# a fast and comprehensive C++ library for automatic differentiation.
#
# Copyright (C) 2010-2026 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 <http://www.gnu.org/licenses/>.
#
##############################################################################

# XAD from the installed xad Python package (shared libxad, tape shared with xad-py).
# XAD_DIR overrides the package lookup. Defines XAD::xad.

if(NOT XAD_DIR)
find_package(Python 3.10 REQUIRED COMPONENTS Interpreter)
execute_process(
COMMAND "${Python_EXECUTABLE}" -c "import xad; print(xad.get_cmake_dir())"
OUTPUT_VARIABLE xad_cmake_dir OUTPUT_STRIP_TRAILING_WHITESPACE
RESULT_VARIABLE xad_rc)
if(NOT xad_rc EQUAL 0)
message(FATAL_ERROR "xad not importable by ${Python_EXECUTABLE}: pip install \"xad>=2.2,<2.3\"")
endif()
file(TO_CMAKE_PATH "${xad_cmake_dir}" xad_cmake_dir)
set(XAD_DIR "${xad_cmake_dir}" CACHE PATH "XAD CMake package directory" FORCE)
endif()

find_package(XAD 2.2 CONFIG REQUIRED)
if(XAD_VERSION VERSION_GREATER_EQUAL 2.3 OR NOT XAD_SHARED_LIBS OR XAD_NO_THREADLOCAL)
message(FATAL_ERROR "XAD ${XAD_VERSION} in ${XAD_DIR}: need xad 2.2.x with shared libxad and "
"thread-local tapes")
endif()
message(STATUS "XAD ${XAD_VERSION}: ${XAD_DIR}")
23 changes: 9 additions & 14 deletions cmake/QuantLibFromSource.cmake
Original file line number Diff line number Diff line change
@@ -1,29 +1,18 @@
# Builds QuantLib as a static library with XAD and QuantLibAAD from lib/.
# Defines QuantLib::QuantLib. With QLR_QUANTLIB_ONLY, builds and installs only these.
# Builds QuantLib as a static library with QuantLibAAD from lib/, against XAD::xad
# (QLRXad). Defines QuantLib::QuantLib. With QLR_QUANTLIB_ONLY, builds and installs only these.

set(QLR_LIB_DIR "${PROJECT_SOURCE_DIR}/lib")

include(QLRBoost)

# XAD: the tape is shared with xad-py, so this must match its configuration
set(XAD_ENABLE_TESTS OFF CACHE BOOL "" FORCE)
set(XAD_NO_THREADLOCAL ON CACHE BOOL "" FORCE)
set(XAD_STATIC_MSVC_RUNTIME ON CACHE BOOL "" FORCE)
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64|amd64)$")
set(XAD_SIMD_OPTION AVX CACHE STRING "XAD SIMD instruction set")
else()
# no arch flags, XAD would pick -mavx on aarch64 Linux
set(XAD_SIMD_OPTION NONE CACHE STRING "XAD SIMD instruction set")
endif()

# QuantLib: static, no tests/examples/benchmark
set(BUILD_SHARED_LIBS OFF)
foreach(opt QL_BUILD_TEST_SUITE QL_BUILD_EXAMPLES QL_BUILD_BENCHMARK
QL_INSTALL_TEST_SUITE QL_INSTALL_EXAMPLES QL_INSTALL_BENCHMARK)
set(${opt} OFF CACHE BOOL "" FORCE)
endforeach()
set(QL_NULL_AS_FUNCTIONS ON CACHE BOOL "" FORCE)
set(QL_EXTERNAL_SUBDIRECTORIES "${QLR_LIB_DIR}/xad;${QLR_LIB_DIR}/QuantLibAAD" CACHE STRING "" FORCE)
set(QL_EXTERNAL_SUBDIRECTORIES "${QLR_LIB_DIR}/QuantLibAAD" CACHE STRING "" FORCE)
set(QL_EXTRA_LINK_LIBRARIES QuantLibAAD CACHE STRING "" FORCE)
# QuantLib's find_package(Boost) (FindBoost module) uses the headers from QLRBoost
set(CMAKE_POLICY_DEFAULT_CMP0167 OLD)
Expand Down Expand Up @@ -65,3 +54,9 @@ if(QLR_BOOST_DOWNLOADED)
install(DIRECTORY "${QLR_BOOST_INCLUDE_DIR}/boost" DESTINATION "${QL_INSTALL_INCLUDEDIR}")
endif()
endif()

if(QLR_QUANTLIB_ONLY)
# checked by PrebuiltQuantLib: the extension must use the same XAD
file(WRITE "${CMAKE_BINARY_DIR}/QLRXadVersion.cmake" "set(QLR_QUANTLIB_XAD_VERSION ${XAD_VERSION})\n")
install(FILES "${CMAKE_BINARY_DIR}/QLRXadVersion.cmake" DESTINATION "${QLAAD_INSTALL_CMAKEDIR}")
endif()
1 change: 0 additions & 1 deletion lib/xad
Submodule xad deleted from 71b381
32 changes: 21 additions & 11 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

[build-system]
# swig 4.5 drops the PyInt_* macros used in SWIG/
requires = ["scikit-build-core>=0.11", "swig>=4.4,<4.5"]
requires = ["scikit-build-core>=0.11", "swig>=4.4,<4.5", "xad>=2.2,<2.3"]
build-backend = "scikit_build_core.build"

[project]
Expand Down Expand Up @@ -70,8 +70,8 @@ classifiers = [
"Topic :: Scientific/Engineering",
"Topic :: Software Development",
]
# the binary module shares xad's tape and Real type: same XAD/pybind11 build
dependencies = ["xad==2.1.*"]
# the binary module links xad's libxad and shares its Real type (same pybind11)
dependencies = ["xad>=2.2,<2.3"]

[project.urls]
Homepage = "https://auto-differentiation.github.io"
Expand Down Expand Up @@ -99,9 +99,6 @@ sdist.exclude = [
"lib/QuantLib/Examples",
"lib/QuantLib/test-suite",
"lib/QuantLib/fuzz-test-suite",
"lib/xad/docs",
"lib/xad/samples",
"lib/xad/test",
"lib/pybind11/docs",
"lib/pybind11/tests",
]
Expand All @@ -111,16 +108,29 @@ build = ["cp310-*", "cp311-*", "cp312-*", "cp313-*", "cp314-*"]
skip = ["cp3??t-*"] # no free-threaded builds
archs = ["native"] # one arch per runner
build-verbosity = 1
# QuantLib (with XAD and QuantLibAAD) is built once per platform and reused
# by all wheels through QLR_QUANTLIB_PREFIX
before-all = "cmake -DPREFIX={project}/build/quantlib-prefix -P {project}/tools/build-quantlib.cmake"
# QuantLib (with QuantLibAAD) is built once per platform and reused by all
# wheels through QLR_QUANTLIB_PREFIX, against the XAD of a xad package that
# build-quantlib.cmake installs (XAD_REQUIREMENT)
before-all = "cmake \"-DXAD_REQUIREMENT=xad @ git+https://github.com/auto-differentiation/xad-py.git@3a69afb69feb70e8965e924f4c13711bab2404e5\" -DPREFIX={project}/build/quantlib-prefix -P {project}/tools/build-quantlib.cmake"
config-settings = { "cmake.define.QLR_QUANTLIB_PREFIX" = "{project}/build/quantlib-prefix" }
test-groups = ["test"]
# xad 2.1 is released together with this package: until then, test against xad-py main
before-test = "pip install \"xad @ git+https://github.com/auto-differentiation/xad-py.git@7961b377f2cbbfec994193f55c23d7be34a72b5c\""
# xad 2.2 is released together with this package: until then, build and test against
# xad-py from git (drop before-build, build-frontend and before-test, and the
# XAD_REQUIREMENT above, once it is on PyPI)
before-build = "pip install \"scikit-build-core>=0.11\" \"swig>=4.4,<4.5\" cmake ninja \"xad @ git+https://github.com/auto-differentiation/xad-py.git@3a69afb69feb70e8965e924f4c13711bab2404e5\""
build-frontend = { name = "build", args = ["--no-isolation"] }
before-test = "pip install \"xad @ git+https://github.com/auto-differentiation/xad-py.git@3a69afb69feb70e8965e924f4c13711bab2404e5\""
# importlib mode tests the installed package; Python/test would clash with the stdlib test package
test-command = "pytest -p no:cacheprovider --import-mode=importlib {project}/Python/test"

# libxad comes from the xad package (rpath to xad/lib), never bundled
[tool.cibuildwheel.linux]
repair-wheel-command = "auditwheel repair --exclude libxad.so -w {dest_dir} {wheel}"

[tool.cibuildwheel.windows]
repair-wheel-command = "pip install delvewheel && delvewheel repair --exclude xad.dll -w {dest_dir} {wheel}"

[tool.cibuildwheel.macos]
repair-wheel-command = "delocate-wheel --require-archs {delocate_archs} --exclude libxad.dylib -w {dest_dir} -v {wheel}"
# one target for the QuantLib build (before-all) and all wheels (raised to 11.0 on arm64)
environment = { MACOSX_DEPLOYMENT_TARGET = "10.15" }
Loading
Loading