From f0dc72b2e4a32fec8665eda765037f0b61b65317 Mon Sep 17 00:00:00 2001 From: Auto Differentiation Dev Team <107129969+auto-differentiation-dev@users.noreply.github.com> Date: Sun, 25 Oct 2026 18:40:42 +0000 Subject: [PATCH 1/4] build: link libxad from the xad 2.2 package Build QuantLib and the extension against the installed xad package (find_package(XAD CONFIG) via xad.get_cmake_dir()) instead of lib/xad, with thread-local tapes and the dynamic MSVC runtime. The extension finds libxad through an rpath to xad/lib; wheel repair excludes it. --- .github/dependabot.yml | 4 +-- .github/workflows/quantlib-risks.yml | 6 +++- .gitmodules | 3 -- CHANGELOG.md | 3 +- CMakeLists.txt | 8 +++-- CONTRIBUTING.md | 14 ++++----- Python/QuantLib_Risks/CMakeLists.txt | 8 ++++- Python/QuantLib_Risks/__init__.py | 2 ++ cmake/PrebuiltQuantLib.cmake | 24 +++++---------- cmake/QLRXad.cmake | 45 ++++++++++++++++++++++++++++ cmake/QuantLibFromSource.cmake | 23 ++++++-------- lib/xad | 1 - pyproject.toml | 31 ++++++++++++------- tools/build-quantlib.cmake | 20 ++++++++++++- 14 files changed, 131 insertions(+), 61 deletions(-) create mode 100644 cmake/QLRXad.cmake delete mode 160000 lib/xad diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d0df47fb..e5f4d566 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,4 +1,4 @@ -# submodules are bumped manually (XAD and pybind11 must match xad) +# submodules are bumped manually (pybind11 must match xad) # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file version: 2 @@ -8,7 +8,7 @@ updates: schedule: interval: "weekly" ignore: - # pinned to the XAD in lib/xad + # one major.minor series, bumped manually - dependency-name: "xad" groups: python: diff --git a/.github/workflows/quantlib-risks.yml b/.github/workflows/quantlib-risks.yml index 90fbf46b..4b15bd08 100644 --- a/.github/workflows/quantlib-risks.yml +++ b/.github/workflows/quantlib-risks.yml @@ -100,7 +100,11 @@ jobs: submodules: recursive persist-credentials: false - name: Build sdist - run: pipx run build --sdist + # no isolation until xad 2.2 is on PyPI + run: | + python3 -m venv /tmp/sdist-env + /tmp/sdist-env/bin/pip install build "scikit-build-core>=0.11" + /tmp/sdist-env/bin/python -m build --sdist --no-isolation --skip-dependency-check - name: Check metadata run: pipx run twine check --strict dist/* - name: Upload sdist diff --git a/.gitmodules b/.gitmodules index 32309105..ad4d6c9a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index c87c9cf7..48ab61a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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/` diff --git a/CMakeLists.txt b/CMakeLists.txt index 56b0fc6e..8c2bf7a1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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$<$:Debug>") +# dynamic MSVC runtime everywhere (QuantLib, the module), as libxad is a DLL +set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$: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() diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e9929364..13f55960 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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. @@ -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 ``` diff --git a/Python/QuantLib_Risks/CMakeLists.txt b/Python/QuantLib_Risks/CMakeLists.txt index 841352ab..f31cc43e 100644 --- a/Python/QuantLib_Risks/CMakeLists.txt +++ b/Python/QuantLib_Risks/CMakeLists.txt @@ -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() diff --git a/Python/QuantLib_Risks/__init__.py b/Python/QuantLib_Risks/__init__.py index d1cd69d8..397f4a8e 100644 --- a/Python/QuantLib_Risks/__init__.py +++ b/Python/QuantLib_Risks/__init__.py @@ -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: diff --git a/cmake/PrebuiltQuantLib.cmake b/cmake/PrebuiltQuantLib.cmake index b65edf99..f3d22cdc 100644 --- a/cmake/PrebuiltQuantLib.cmake +++ b/cmake/PrebuiltQuantLib.cmake @@ -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}") diff --git a/cmake/QLRXad.cmake b/cmake/QLRXad.cmake new file mode 100644 index 00000000..fe29a387 --- /dev/null +++ b/cmake/QLRXad.cmake @@ -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 . +# +############################################################################## + +# 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}") diff --git a/cmake/QuantLibFromSource.cmake b/cmake/QuantLibFromSource.cmake index 9fdd2cb3..87ababab 100644 --- a/cmake/QuantLibFromSource.cmake +++ b/cmake/QuantLibFromSource.cmake @@ -1,21 +1,10 @@ -# 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 @@ -23,7 +12,7 @@ foreach(opt QL_BUILD_TEST_SUITE QL_BUILD_EXAMPLES QL_BUILD_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) @@ -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() diff --git a/lib/xad b/lib/xad deleted file mode 160000 index 71b381ad..00000000 --- a/lib/xad +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 71b381ad4a322d7ed5eb8b2b4b79a20c92c646c6 diff --git a/pyproject.toml b/pyproject.toml index 71cbb300..fe0d2596 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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] @@ -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" @@ -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", ] @@ -111,16 +108,28 @@ 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@d80acfa10a2e5fbe6c67c7d4f3abcd0b96f52534\" -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\"" +# until xad 2.2 is on PyPI: xad-py from git (then drop before-build, build-frontend, +# before-test and XAD_REQUIREMENT) +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@d80acfa10a2e5fbe6c67c7d4f3abcd0b96f52534\"" +build-frontend = { name = "build", args = ["--no-isolation"] } +before-test = "pip install \"xad @ git+https://github.com/auto-differentiation/xad-py.git@d80acfa10a2e5fbe6c67c7d4f3abcd0b96f52534\"" # 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} --ignore-missing-dependencies --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" } diff --git a/tools/build-quantlib.cmake b/tools/build-quantlib.cmake index dee64314..bfab1caf 100644 --- a/tools/build-quantlib.cmake +++ b/tools/build-quantlib.cmake @@ -40,6 +40,24 @@ foreach(dir PREFIX BUILD_DIR) get_filename_component(${dir} "${${dir}}" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_BINARY_DIR}") endforeach() +if(NOT XAD_DIR) + if(NOT PYTHON) + find_program(PYTHON NAMES python3.14 python3.13 python3.12 python3.11 python3.10 python3 python + REQUIRED) + endif() + if(XAD_REQUIREMENT) + set(venv "${BUILD_DIR}/xad-venv") + execute_process(COMMAND "${PYTHON}" -m venv "${venv}" COMMAND_ERROR_IS_FATAL ANY) + find_program(venv_python NAMES python python3 PATHS "${venv}/bin" "${venv}/Scripts" + NO_DEFAULT_PATH NO_CACHE REQUIRED) + set(PYTHON "${venv_python}") + execute_process(COMMAND "${PYTHON}" -m pip install "${XAD_REQUIREMENT}" COMMAND_ERROR_IS_FATAL ANY) + endif() + execute_process(COMMAND "${PYTHON}" -c "import xad; print(xad.get_cmake_dir())" + OUTPUT_VARIABLE XAD_DIR OUTPUT_STRIP_TRAILING_WHITESPACE COMMAND_ERROR_IS_FATAL ANY) +endif() +file(TO_CMAKE_PATH "${XAD_DIR}" XAD_DIR) + if(DEFINED ENV{CMAKE_BUILD_PARALLEL_LEVEL}) set(jobs $ENV{CMAKE_BUILD_PARALLEL_LEVEL}) else() @@ -47,7 +65,7 @@ else() endif() foreach(step - "-S;${source_dir};-B;${BUILD_DIR};-DQLR_QUANTLIB_ONLY=ON;-DCMAKE_BUILD_TYPE=Release;-DCMAKE_INSTALL_PREFIX=${PREFIX}" + "-S;${source_dir};-B;${BUILD_DIR};-DQLR_QUANTLIB_ONLY=ON;-DCMAKE_BUILD_TYPE=Release;-DCMAKE_INSTALL_PREFIX=${PREFIX};-DXAD_DIR=${XAD_DIR}" "--build;${BUILD_DIR};--config;Release;--parallel;${jobs}" "--install;${BUILD_DIR};--config;Release") execute_process(COMMAND ${CMAKE_COMMAND} ${step} COMMAND_ERROR_IS_FATAL ANY) From b76003a3999317d097912aee7a34c524d3073b26 Mon Sep 17 00:00:00 2001 From: Auto Differentiation Dev Team <107129969+auto-differentiation-dev@users.noreply.github.com> Date: Sun, 25 Oct 2026 18:40:42 +0000 Subject: [PATCH 2/4] remove the tape activation monkey-patch QuantLib and xad share the active tape through libxad, so xad.Tape no longer needs wrapping. --- Python/QuantLib_Risks/__init__.py | 35 ------------------------------- SWIG/tape.i | 14 +------------ 2 files changed, 1 insertion(+), 48 deletions(-) diff --git a/Python/QuantLib_Risks/__init__.py b/Python/QuantLib_Risks/__init__.py index 397f4a8e..9263c3cc 100644 --- a/Python/QuantLib_Risks/__init__.py +++ b/Python/QuantLib_Risks/__init__.py @@ -54,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__"): diff --git a/SWIG/tape.i b/SWIG/tape.i index 634870ae..b0a2fc96 100644 --- a/SWIG/tape.i +++ b/SWIG/tape.i @@ -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&. * * This file is part of QuantLib-Risks, a Python wrapper for QuantLib enabled * for risk computation using automatic differentiation. It uses XAD, @@ -30,14 +29,6 @@ #include "converters.hpp" #include - void _activate_tape(xad::Tape& t) { - xad::Tape::setActive(&t); - } - - void _deactivate_tape() { - xad::Tape::deactivateAll(); - } - typedef xad::Tape Tape; %} @@ -59,7 +50,4 @@ typedef xad::Tape Tape; $1 = check_Tape($input) ? 1 : 0; } -void _activate_tape(xad::Tape& t); -void _deactivate_tape(); - #endif From ca04b7c9e360463d69f350def583b7d845ad1d0f Mon Sep 17 00:00:00 2001 From: Auto Differentiation Dev Team <107129969+auto-differentiation-dev@users.noreply.github.com> Date: Mon, 26 Oct 2026 19:22:07 +0000 Subject: [PATCH 3/4] build: accept the xad 2.1 shared-library build until xad 2.2 is released --- cmake/QLRXad.cmake | 6 +++--- pyproject.toml | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/cmake/QLRXad.cmake b/cmake/QLRXad.cmake index fe29a387..67c715a5 100644 --- a/cmake/QLRXad.cmake +++ b/cmake/QLRXad.cmake @@ -31,15 +31,15 @@ if(NOT XAD_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\"") + message(FATAL_ERROR "xad not importable by ${Python_EXECUTABLE}: pip install \"xad>=2.1,<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) +find_package(XAD 2.1 CONFIG REQUIRED) # 2.2 once released 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 " + message(FATAL_ERROR "XAD ${XAD_VERSION} in ${XAD_DIR}: need xad with shared libxad and " "thread-local tapes") endif() message(STATUS "XAD ${XAD_VERSION}: ${XAD_DIR}") diff --git a/pyproject.toml b/pyproject.toml index fe0d2596..f35e7464 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", "xad>=2.2,<2.3"] +requires = ["scikit-build-core>=0.11", "swig>=4.4,<4.5", "xad>=2.1,<2.3"] build-backend = "scikit_build_core.build" [project] @@ -71,7 +71,7 @@ classifiers = [ "Topic :: Software Development", ] # the binary module links xad's libxad and shares its Real type (same pybind11) -dependencies = ["xad>=2.2,<2.3"] +dependencies = ["xad>=2.1,<2.3"] [project.urls] Homepage = "https://auto-differentiation.github.io" @@ -111,14 +111,14 @@ build-verbosity = 1 # 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@d80acfa10a2e5fbe6c67c7d4f3abcd0b96f52534\" -DPREFIX={project}/build/quantlib-prefix -P {project}/tools/build-quantlib.cmake" +before-all = "cmake \"-DXAD_REQUIREMENT=xad @ git+https://github.com/auto-differentiation/xad-py.git@bf056196040684c6a925180d64be1ac938b62369\" -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"] -# until xad 2.2 is on PyPI: xad-py from git (then drop before-build, build-frontend, +# until xad 2.2 is on PyPI (then also xad>=2.2): xad-py from git (then drop before-build, build-frontend, # before-test and XAD_REQUIREMENT) -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@d80acfa10a2e5fbe6c67c7d4f3abcd0b96f52534\"" +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@bf056196040684c6a925180d64be1ac938b62369\"" build-frontend = { name = "build", args = ["--no-isolation"] } -before-test = "pip install \"xad @ git+https://github.com/auto-differentiation/xad-py.git@d80acfa10a2e5fbe6c67c7d4f3abcd0b96f52534\"" +before-test = "pip install \"xad @ git+https://github.com/auto-differentiation/xad-py.git@bf056196040684c6a925180d64be1ac938b62369\"" # 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" From dfd7eca3c21518c47f3377e1b3ad5fe22921da5b Mon Sep 17 00:00:00 2001 From: Auto Differentiation Dev Team <107129969+auto-differentiation-dev@users.noreply.github.com> Date: Thu, 29 Oct 2026 16:36:23 +0000 Subject: [PATCH 4/4] ci: xad-py main with the shared libxad and the 2.2.0 features The wheels and their tests install xad from git until xad 2.2 is on PyPI. The pin moves from bf05619, before the shared libxad was merged in xad-py (#42), to current xad-py main 9ca1797: shared libxad, complex numbers, checkpointing, external functions, NumPy arrays and the fix for inputs reused on a new tape, i.e. the 2.2.0 content. --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f35e7464..5391ca2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -111,14 +111,14 @@ build-verbosity = 1 # 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@bf056196040684c6a925180d64be1ac938b62369\" -DPREFIX={project}/build/quantlib-prefix -P {project}/tools/build-quantlib.cmake" +before-all = "cmake \"-DXAD_REQUIREMENT=xad @ git+https://github.com/auto-differentiation/xad-py.git@9ca179764907ba44038efd1beb3631d35a5e22ea\" -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"] # until xad 2.2 is on PyPI (then also xad>=2.2): xad-py from git (then drop before-build, build-frontend, # before-test and XAD_REQUIREMENT) -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@bf056196040684c6a925180d64be1ac938b62369\"" +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@9ca179764907ba44038efd1beb3631d35a5e22ea\"" build-frontend = { name = "build", args = ["--no-isolation"] } -before-test = "pip install \"xad @ git+https://github.com/auto-differentiation/xad-py.git@bf056196040684c6a925180d64be1ac938b62369\"" +before-test = "pip install \"xad @ git+https://github.com/auto-differentiation/xad-py.git@9ca179764907ba44038efd1beb3631d35a5e22ea\"" # 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"