diff --git a/.gersemirc b/.gersemirc index f2f71eb..ebecb14 100644 --- a/.gersemirc +++ b/.gersemirc @@ -1,4 +1,4 @@ -definitions: [./CMakeLists.txt, ./cmake, ./tests] -line_length: 80 +definitions: [./CMakeLists.txt, ./tests] +line_length: 100 indent: 2 warn_about_unknown_commands: false diff --git a/.github/workflows/macos-linux-windows-pixi.yml b/.github/workflows/macos-linux-windows-pixi.yml index 850a32a..c902bd8 100644 --- a/.github/workflows/macos-linux-windows-pixi.yml +++ b/.github/workflows/macos-linux-windows-pixi.yml @@ -55,8 +55,6 @@ jobs: steps: - uses: actions/checkout@v6 - with: - submodules: recursive - uses: actions/cache@v4 with: @@ -73,19 +71,23 @@ jobs: run: | pixi run -e ${{ matrix.environment }} ccache -z - - name: Build nanoeigenpy [MacOS/Linux/Windows] + - name: Configure nanoeigenpy [MacOS/Linux/Windows] env: NANOEIGENPY_BUILD_TYPE: ${{ matrix.build_type }} + run: | + pixi run -e ${{ matrix.environment }} configure + + - name: Build nanoeigenpy [MacOS/Linux/Windows] run: | pixi run -e ${{ matrix.environment }} build - name: Test nanoeigenpy [MacOS/Linux/Windows] run: | - pixi run -e ${{ matrix.environment }} ctest --test-dir build --output-on-failure + pixi run -e ${{ matrix.environment }} test - name: Install nanoeigenpy [MacOS/Linux/Windows] run: | - pixi run -e ${{ matrix.environment }} cmake --build build --target install + pixi run -e ${{ matrix.environment }} install - name: Show ccache statistics [MacOS/Linux/Windows] run: | @@ -102,14 +104,12 @@ jobs: steps: - uses: actions/checkout@v6 - with: - submodules: recursive - uses: prefix-dev/setup-pixi@v0.9.3 env: CMAKE_BUILD_PARALLEL_LEVEL: 2 with: - cache: true + cache: false # ⚠️ Disabling cache for testing ⚠️ environments: test-pixi-build - name: Test package [MacOS/Linux/Windows] diff --git a/.github/workflows/ros_ci.yml b/.github/workflows/ros_ci.yml index a5addae..95d9dee 100644 --- a/.github/workflows/ros_ci.yml +++ b/.github/workflows/ros_ci.yml @@ -31,11 +31,12 @@ jobs: env: # PRERELEASE: true # Fails due to issues in the underlying Docker image BUILDER: colcon + VERBOSE_OUTPUT: true + VERBOSE_TESTS: true + DEBUG_BASH: true runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - with: - submodules: recursive # Run industrial_ci - uses: 'ros-industrial/industrial_ci@ba2a3d0f830f8051b356711a8df2fedfc5d256cf' env: ${{ matrix.env }} diff --git a/.gitignore b/.gitignore index 36de0b5..6aaa80b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,16 @@ build*/ +install*/ +.pytest_cache/ .cache/ -__pycache__ -.pytest_cache - -# pixi environments -.pixi +.pixi/ +__pycache__/ +Xcode* +*.pyc +*~ *.egg-info +.ruff_cache +.DS_Store +compile_commands.json +cmake-profiling.json +result +*.conda diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 955a435..0000000 --- a/.gitmodules +++ /dev/null @@ -1,6 +0,0 @@ -[submodule "cmake"] - path = cmake - url = https://github.com/jrl-umi3218/jrl-cmakemodules.git -[submodule "ext/nanobind"] - path = ext/nanobind - url = https://github.com/wjakob/nanobind diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a0c6a2..3ab5117 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Python version update ([#25](https://github.com/Simple-Robotics/nanoeigenpy/pull/25)): - Project is now tested with Python 3.10 and 3.14 - Python 3.10 is the minimal supported Python version +- Switch to [JRL CMake modules v2](https://github.com/jrl-umi3218/jrl-cmakemodules/pull/798) ([#28](https://github.com/Simple-Robotics/nanoeigenpy/pull/28)) ### Added - Add pixi-build support ([#25](https://github.com/Simple-Robotics/nanoeigenpy/pull/25)) diff --git a/CMakeLists.txt b/CMakeLists.txt index f18aaa6..cbb34d6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,248 +1,241 @@ -# -# Copyright 2025 INRIA -# +cmake_minimum_required(VERSION 3.22...4.2) -cmake_minimum_required(VERSION 3.22) +project( + nanoeigenpy + VERSION 0.12.0 + DESCRIPTION "A support library for bindings between Eigen in C++ and Python, based on nanobind" + HOMEPAGE_URL "https://github.com/Simple-Robotics/nanoeigenpy" +) -set(PROJECT_NAME nanoeigenpy) -set(PROJECT_URL https://github.com/Simple-Robotics/nanoeigenpy) -set( - PROJECT_DESCRIPTION - "A support library for bindings between Eigen in C++ and Python, based on nanobind" +include(cmake/get-jrl-cmakemodules.cmake) + +jrl_configure_defaults() + +jrl_option(BUILD_TESTING "Build the tests" OFF) + +jrl_option(INSTALL_DOCUMENTATION "Generate and install the documentation" OFF) + +jrl_option(BUILD_WITH_CHOLMOD_SUPPORT + "Build EigenPy with the Cholmod (LGPL) support. See CHOLMOD/Doc/License.txt for further details." OFF ) -set(PROJECT_CUSTOM_HEADER_EXTENSION "hpp") -set(PROJECT_USE_CMAKE_EXPORT True) - -# To enable jrl-cmakemodules compatibility with workspace we must define the two -# following lines -set(PROJECT_AUTO_RUN_FINALIZE FALSE) -set(PROJECT_SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}) - -# Check if the submodule cmake have been initialized -set(JRL_CMAKE_MODULES "${CMAKE_CURRENT_LIST_DIR}/cmake") -if(EXISTS "${JRL_CMAKE_MODULES}/base.cmake") - message(STATUS "JRL cmakemodules found in 'cmake/' git submodule") -else() - find_package(jrl-cmakemodules QUIET CONFIG) - if(jrl-cmakemodules_FOUND) - get_property( - JRL_CMAKE_MODULES - TARGET jrl-cmakemodules::jrl-cmakemodules - PROPERTY INTERFACE_INCLUDE_DIRECTORIES - ) - message(STATUS "JRL cmakemodules found on system at ${JRL_CMAKE_MODULES}") - elseif(${CMAKE_VERSION} VERSION_LESS "3.14.0") - message( - FATAL_ERROR - "\nCan't find jrl-cmakemodules. Please either:\n" - " - use git submodule: 'git submodule update --init'\n" - " - or install https://github.com/jrl-umi3218/jrl-cmakemodules\n" - " - or upgrade your CMake version to >= 3.14 to allow automatic fetching\n" - ) - else() - message(STATUS "JRL cmakemodules not found. Let's fetch it.") - include(FetchContent) - FetchContent_Declare( - "jrl-cmakemodules" - GIT_REPOSITORY "https://github.com/jrl-umi3218/jrl-cmakemodules.git" - ) - FetchContent_MakeAvailable("jrl-cmakemodules") - FetchContent_GetProperties("jrl-cmakemodules" SOURCE_DIR JRL_CMAKE_MODULES) - endif() -endif() -option(INSTALL_DOCUMENTATION "Generate and install the documentation" OFF) +jrl_option(BUILD_WITH_ACCELERATE_SUPPORT + "Build EigenPy with the Accelerate support (Apple only)" OFF +) -if(POLICY CMP0167) - cmake_policy(SET CMP0167 NEW) - set(CMAKE_POLICY_DEFAULT_CMP0167 NEW) +if(BUILD_WITH_ACCELERATE_SUPPORT AND NOT APPLE) + message(WARNING "Accelerate support is only available on APPLE systems") endif() -if(POLICY CMP0177) - cmake_policy(SET CMP0177 NEW) - set(CMAKE_POLICY_DEFAULT_CMP0177 NEW) -endif() -include("${JRL_CMAKE_MODULES}/base.cmake") -COMPUTE_PROJECT_ARGS(PROJECT_ARGS LANGUAGES CXX) -include("${JRL_CMAKE_MODULES}/ide.cmake") -include("${JRL_CMAKE_MODULES}/apple.cmake") -project(${PROJECT_NAME} ${PROJECT_ARGS}) - -string(REPLACE "-pedantic" "" CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS}) -string(REPLACE "-Wcast-qual" "" CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS}) -string(REPLACE "-Wconversion" "" CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS}) - -set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin) -set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib) -set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib) - -if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) - set(CMAKE_BUILD_TYPE Release CACHE STRING "Choose the type of build." FORCE) - set_property( - CACHE CMAKE_BUILD_TYPE - PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo" - ) + +jrl_find_package(Eigen3 CONFIG REQUIRED) + +jrl_find_python(3.8 REQUIRED COMPONENTS Interpreter Development.Module) +jrl_find_nanobind(2.5.0 CONFIG QUIET) + +if(nanobind_ROOT AND NOT nanobind_FOUND) + message(WARNING "nanobind found at ${nanobind_ROOT} but too old") endif() -option( - BUILD_WITH_CHOLMOD_SUPPORT - "Build NanoEigenPy with the Cholmod support" - OFF -) +# On Ubuntu 24.04, the nanobind-dev package ships nanobind 1.9.2. +# We require >=2.5.0 for nanobind_add_stub, +# NB_SUPPRESS_WARNINGS, and visitor pattern (c++) +if(NOT nanobind_FOUND) + set(nanobind_GIT_REPOSITORY "https://github.com/wjakob/nanobind.git") + set(nanobind_GIT_TAG "v2.10.1") -if(APPLE) - option( - BUILD_WITH_ACCELERATE_SUPPORT - "Build EigenPy with the Accelerate support" - OFF + message( + STATUS + "nanobind: fallback to FetchContent from ${nanobind_GIT_REPOSITORY} (tag: ${nanobind_GIT_TAG})" ) -endif(APPLE) -# Find dependencies -ADD_PROJECT_DEPENDENCY(Eigen3 REQUIRED PKG_CONFIG_REQUIRES "eigen3 >= 3.3.1") + include(FetchContent) + FetchContent_Declare( + nanobind + GIT_REPOSITORY ${nanobind_GIT_REPOSITORY} + GIT_TAG ${nanobind_GIT_TAG} + GIT_SHALLOW True + ) + FetchContent_MakeAvailable(nanobind) +endif() -find_package(Python REQUIRED COMPONENTS Interpreter Development) -# On Windows Python_SITELIB contains \ that can create installation issues -if(WIN32) - string(REPLACE "\\" "/" Python_SITELIB "${Python_SITELIB}") +if(BUILD_WITH_CHOLMOD_SUPPORT) + jrl_find_package(CHOLMOD CONFIG REQUIRED) endif() -# Detect the installed nanobind package and import it into CMake -execute_process( - COMMAND "${Python_EXECUTABLE}" -m nanobind --cmake_dir - OUTPUT_STRIP_TRAILING_WHITESPACE - OUTPUT_VARIABLE nanobind_ROOT -) -find_package(nanobind 2.5.0 CONFIG) -if(NOT nanobind_FOUND) - add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/ext/nanobind) +if(BUILD_WITH_ACCELERATE_SUPPORT AND APPLE) + jrl_find_package(Accelerate REQUIRED) endif() -# Setup main targets -file( - GLOB_RECURSE ${PROJECT_NAME}_HEADERS - CONFIGURE_DEPENDS - include/nanoeigenpy/*.hpp -) +if(BUILD_TESTING) + jrl_find_package(Pytest REQUIRED) +endif() -add_library(nanoeigenpy_headers INTERFACE) -target_include_directories( - nanoeigenpy_headers - INTERFACE - $ - $ - $ +set( + nanoeigenpy_HEADERS + include/nanoeigenpy/decompositions/sparse/simplicial-cholesky.hpp + include/nanoeigenpy/decompositions/sparse/simplicial-ldlt.hpp + include/nanoeigenpy/decompositions/sparse/simplicial-llt.hpp + include/nanoeigenpy/decompositions/sparse/sparse-lu.hpp + include/nanoeigenpy/decompositions/sparse/sparse-qr.hpp + include/nanoeigenpy/decompositions/sparse/sparse-solver-base.hpp + include/nanoeigenpy/decompositions/bdcsvd.hpp + include/nanoeigenpy/decompositions/col-piv-householder-qr.hpp + include/nanoeigenpy/decompositions/complete-orthogonal-decomposition.hpp + include/nanoeigenpy/decompositions/complex-eigen-solver.hpp + include/nanoeigenpy/decompositions/complex-schur.hpp + include/nanoeigenpy/decompositions/eigen-solver.hpp + include/nanoeigenpy/decompositions/full-piv-householder-qr.hpp + include/nanoeigenpy/decompositions/full-piv-lu.hpp + include/nanoeigenpy/decompositions/generalized-eigen-solver.hpp + include/nanoeigenpy/decompositions/generalized-self-adjoint-eigen-solver.hpp + include/nanoeigenpy/decompositions/hessenberg-decomposition.hpp + include/nanoeigenpy/decompositions/householder-qr.hpp + include/nanoeigenpy/decompositions/jacobi-svd.hpp + include/nanoeigenpy/decompositions/ldlt.hpp + include/nanoeigenpy/decompositions/llt.hpp + include/nanoeigenpy/decompositions/partial-piv-lu.hpp + include/nanoeigenpy/decompositions/permutation-matrix.hpp + include/nanoeigenpy/decompositions/real-qz.hpp + include/nanoeigenpy/decompositions/real-schur.hpp + include/nanoeigenpy/decompositions/self-adjoint-eigen-solver.hpp + include/nanoeigenpy/decompositions/svd-base.hpp + include/nanoeigenpy/decompositions/tridiagonalization.hpp + include/nanoeigenpy/geometry/detail/rotation-base.hpp + include/nanoeigenpy/geometry/angle-axis.hpp + include/nanoeigenpy/geometry/hyperplane.hpp + include/nanoeigenpy/geometry/jacobi-rotation.hpp + include/nanoeigenpy/geometry/parametrized-line.hpp + include/nanoeigenpy/geometry/quaternion.hpp + include/nanoeigenpy/geometry/rotation-2d.hpp + include/nanoeigenpy/geometry/scaling.hpp + include/nanoeigenpy/geometry/translation.hpp + include/nanoeigenpy/solvers/basic-preconditioners.hpp + include/nanoeigenpy/solvers/bfgs-preconditioners.hpp + include/nanoeigenpy/solvers/bicgstab.hpp + include/nanoeigenpy/solvers/conjugate-gradient.hpp + include/nanoeigenpy/solvers/incomplete-cholesky.hpp + include/nanoeigenpy/solvers/incomplete-lut.hpp + include/nanoeigenpy/solvers/iterative-solver-base.hpp + include/nanoeigenpy/solvers/least-squares-conjugate-gradient.hpp + include/nanoeigenpy/solvers/minres.hpp + include/nanoeigenpy/utils/helpers.hpp + include/nanoeigenpy/utils/is-approx.hpp + include/nanoeigenpy/constants.hpp + include/nanoeigenpy/decompositions.hpp + include/nanoeigenpy/eigen-base.hpp + include/nanoeigenpy/fwd.hpp + include/nanoeigenpy/geometry.hpp + include/nanoeigenpy/id.hpp + include/nanoeigenpy/nanoeigenpy.hpp + include/nanoeigenpy/solvers.hpp ) -target_link_libraries(nanoeigenpy_headers INTERFACE Eigen3::Eigen) - -set(${PROJECT_NAME}_SOURCES src/module.cpp) -nanobind_add_module(nanoeigenpy NB_STATIC NB_SUPPRESS_WARNINGS ${nanoeigenpy_SOURCES} ${nanoeigenpy_HEADERS}) -target_link_libraries(nanoeigenpy PRIVATE nanoeigenpy_headers) -# Cholmod if(BUILD_WITH_CHOLMOD_SUPPORT) - set( - CMAKE_MODULE_PATH - ${JRL_CMAKE_MODULES}/find-external/CHOLMOD - ${CMAKE_MODULE_PATH} - ) - ADD_PROJECT_DEPENDENCY(CHOLMOD REQUIRED FIND_EXTERNAL "CHOLMOD") - message( - STATUS - "Build with CHOLMOD support (LGPL). See CHOLMOD/Doc/License.txt for further details." - ) - file( - GLOB ${PROJECT_NAME}_DECOMPOSITIONS_SPARSE_CHOLMOD_HEADERS - include/nanoeigenpy/decompositions/sparse/cholmod/*.hpp - ) list( APPEND - ${PROJECT_NAME}_HEADERS - ${${PROJECT_NAME}_DECOMPOSITIONS_SPARSE_CHOLMOD_HEADERS} - ) - target_link_libraries(nanoeigenpy PRIVATE CHOLMOD::CHOLMOD) -else() - list( - FILTER ${PROJECT_NAME}_HEADERS - EXCLUDE - REGEX "include/nanoeigenpy/decompositions/sparse/cholmod/.*" + nanoeigenpy_HEADERS + include/nanoeigenpy/decompositions/sparse/cholmod/cholmod-base.hpp + include/nanoeigenpy/decompositions/sparse/cholmod/cholmod-decomposition.hpp + include/nanoeigenpy/decompositions/sparse/cholmod/cholmod-simplicial-ldlt.hpp + include/nanoeigenpy/decompositions/sparse/cholmod/cholmod-simplicial-llt.hpp + include/nanoeigenpy/decompositions/sparse/cholmod/cholmod-supernodal-llt.hpp ) -endif(BUILD_WITH_CHOLMOD_SUPPORT) - -# Apple accelerate -if(BUILD_WITH_ACCELERATE_SUPPORT) - if(NOT ${Eigen3_VERSION} VERSION_GREATER_EQUAL "3.4.90") - message( - FATAL_ERROR - "Your version of Eigen is too low. Should be at least 3.4.90. Current version is ${Eigen3_VERSION}." - ) - endif() - - set( - CMAKE_MODULE_PATH - ${JRL_CMAKE_MODULES}/find-external/Accelerate - ${CMAKE_MODULE_PATH} - ) - find_package(Accelerate REQUIRED) - message(STATUS "Build with Accelerate support framework.") - target_compile_definitions( - nanoeigenpy_headers - INTERFACE -DNANOEIGENPY_WITH_ACCELERATE_SUPPORT - ) -endif(BUILD_WITH_ACCELERATE_SUPPORT) +endif() if(BUILD_WITH_ACCELERATE_SUPPORT) - file( - GLOB ${PROJECT_NAME}_DECOMPOSITIONS_SPARSE_ACCELERATE_HEADERS - include/nanoeigenpy/decompositions/sparse/accelerate/*.hpp - ) list( APPEND - ${PROJECT_NAME}_HEADERS - ${${PROJECT_NAME}_DECOMPOSITIONS_SPARSE_ACCELERATE_HEADERS} - ) -else() - list( - FILTER ${PROJECT_NAME}_HEADERS - EXCLUDE - REGEX "include/nanoeigenpy/decompositions/sparse/accelerate/.*" + nanoeigenpy_HEADERS + include/nanoeigenpy/decompositions/sparse/accelerate/accelerate.hpp ) -endif(BUILD_WITH_ACCELERATE_SUPPORT) - -if(BUILD_WITH_ACCELERATE_SUPPORT) - target_link_libraries(nanoeigenpy PRIVATE Accelerate) endif() -if(BUILD_TESTING) - add_subdirectory(tests) -endif() +# ----------------------------------------------------------------------- # + +add_library(nanoeigenpy_headers INTERFACE) +add_library(nanoeigenpy::nanoeigenpy_headers ALIAS nanoeigenpy_headers) +target_compile_features(nanoeigenpy_headers INTERFACE cxx_std_17) -nanobind_add_stub( - nanoeigenpy_stub - INSTALL_TIME - VERBOSE - MODULE nanoeigenpy - OUTPUT ${Python_SITELIB}/nanoeigenpy.pyi - PYTHON_PATH $ +target_include_directories( + nanoeigenpy_headers + INTERFACE + $ + $ ) -# Install targets -install( - TARGETS ${PROJECT_NAME}_headers - EXPORT ${TARGETS_EXPORT_NAME} - PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} - INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} - LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} - ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} - RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} +set(nanoeigenpy_SOURCES src/module.cpp) + +# NB_SUPPRESS_WARNINGS appeard on nanobind >= 2.5.0 +if(nanobind_VERSION VERSION_GREATER_EQUAL "2.5.0") + set(nb_suppress_warnings "NB_SUPPRESS_WARNINGS") +endif() + +nanobind_add_module(nanoeigenpy + NB_STATIC LTO ${nb_suppress_warnings} + ${nanoeigenpy_SOURCES} + ${nanoeigenpy_HEADERS} ) +jrl_target_set_output_directory(nanoeigenpy OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib/site-packages) + +jrl_target_enforce_msvc_conformance(nanoeigenpy PRIVATE) +jrl_target_generate_config_header(nanoeigenpy PRIVATE VERSION ${PROJECT_VERSION}) +jrl_target_generate_warning_header(nanoeigenpy PRIVATE) +jrl_target_generate_deprecated_header(nanoeigenpy PRIVATE) +jrl_check_python_module_name(nanoeigenpy) +target_link_libraries(nanoeigenpy PRIVATE nanoeigenpy_headers Eigen3::Eigen) + +if(BUILD_WITH_CHOLMOD_SUPPORT) + target_link_libraries(nanoeigenpy PRIVATE SuiteSparse::CHOLMOD) + target_compile_definitions(nanoeigenpy PRIVATE NANOEIGENPY_HAS_CHOLMOD) +endif() + +if(BUILD_WITH_ACCELERATE_SUPPORT AND APPLE) + target_link_libraries(nanoeigenpy PRIVATE Accelerate::Accelerate) + target_compile_definitions(nanoeigenpy PRIVATE NANOEIGENPY_HAS_ACCELERATE) +endif() + +# Stub generation requires typing-extensions +# ROS Humble ships an incompatible typing-extensions with python3.10 +if(Python_VERSION VERSION_GREATER_EQUAL 3.11.0) + nanobind_add_stub( + nanoeigenpy_stub + VERBOSE + MODULE nanoeigenpy + OUTPUT ${CMAKE_BINARY_DIR}/lib/site-packages/nanoeigenpy.pyi + PYTHON_PATH $ + DEPENDS nanoeigenpy + ) +endif() + +jrl_python_compute_install_dir(python_install_dir) +# NOTE: install the whole binary dir, we need the "/" at the end to copy the content. +# In a next version, we might install in a nanoeigenpy directory, which contains all the files. install( - TARGETS ${PROJECT_NAME} - EXPORT ${TARGETS_EXPORT_NAME} - LIBRARY DESTINATION ${Python_SITELIB} + DIRECTORY ${CMAKE_BINARY_DIR}/lib/site-packages/ + DESTINATION ${python_install_dir} + FILES_MATCHING + PATTERN "*.py" + PATTERN "*.pyc" + PATTERN "*.pyi" + PATTERN "*.typed" + PATTERN "*.so" + PATTERN "*.pyd" +) +# ------------------------------------------------------------------------ # +jrl_target_headers(nanoeigenpy_headers INTERFACE + HEADERS ${nanoeigenpy_HEADERS} + BASE_DIRS include ) -ADD_HEADER_GROUP(${PROJECT_NAME}_HEADERS) -ADD_SOURCE_GROUP(${PROJECT_NAME}_SOURCES) +jrl_add_export_component(NAME nanoeigenpy_headers TARGETS nanoeigenpy_headers) +jrl_export_package() + +# ------------------------------------------------------------------------ # +if(BUILD_TESTING) + enable_testing() + add_subdirectory(tests) +endif() -SETUP_PROJECT_FINALIZE() +jrl_print_dependencies_summary() +jrl_print_options_summary() diff --git a/cmake b/cmake deleted file mode 160000 index a3b7cb9..0000000 --- a/cmake +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a3b7cb9a4732f4aae34b4c14d72b04c3fa575ed3 diff --git a/cmake/get-jrl-cmakemodules.cmake b/cmake/get-jrl-cmakemodules.cmake new file mode 100644 index 0000000..1a0a5a0 --- /dev/null +++ b/cmake/get-jrl-cmakemodules.cmake @@ -0,0 +1,50 @@ +# Get jrl-cmakemodules package + +# Upstream (https://github.com/jrl-umi3218/jrl-cmakemodules), the new v2 version is located in a subfolder, +# We need to set this variable to bypass the v1 and load the v2. +set( + JRL_CMAKEMODULES_USE_V2 + ON + CACHE BOOL + "Use jrl-cmakemodules v2 on https://github.com/jrl-umi3218/jrl-cmakemodules" +) + +# Option 1: pass -DJRL_CMAKEMODULES_SOURCE_DIR=... to cmake command line +if(JRL_CMAKEMODULES_SOURCE_DIR) + message( + STATUS + "JRL_CMAKEMODULES_SOURCE_DIR variable set, adding jrl-cmakemodules from source directory: ${JRL_CMAKEMODULES_SOURCE_DIR}" + ) + add_subdirectory(${JRL_CMAKEMODULES_SOURCE_DIR} jrl-cmakemodules) + return() +endif() + +# Option 2: use JRL_CMAKEMODULES_SOURCE_DIR environment variable (pixi might unset it, prefer option 1) +if(ENV{JRL_CMAKEMODULES_SOURCE_DIR}) + message( + STATUS + "JRL_CMAKEMODULES_SOURCE_DIR environement variable set, adding jrl-cmakemodules from source directory: $ENV{JRL_CMAKEMODULES_SOURCE_DIR}" + ) + add_subdirectory($ENV{JRL_CMAKEMODULES_SOURCE_DIR} jrl-cmakemodules) + return() +endif() + +# Option 3: Try to look for the installed package +message(STATUS "Looking for jrl-cmakemodules (version: >=1.1.2) package...") +find_package(jrl-cmakemodules 1.1.2 CONFIG QUIET) + +# If we have the package, we are done here. +if(jrl-cmakemodules_FOUND) + message(STATUS "Found jrl-cmakemodules (version: ${jrl-cmakemodules_VERSION}) package.") + return() +endif() + +# Option 4: Fallback to FetchContent +message(STATUS "Fetching jrl-cmakemodules using FetchContent...") +include(FetchContent) +FetchContent_Declare( + jrl-cmakemodules + GIT_REPOSITORY https://github.com/ahoarau/jrl-cmakemodules + GIT_TAG jrl-next +) +FetchContent_MakeAvailable(jrl-cmakemodules) diff --git a/ext/nanobind b/ext/nanobind deleted file mode 160000 index 879bca4..0000000 --- a/ext/nanobind +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 879bca4869664bdc1446ee7f160ffe3c7028cd7a diff --git a/flake.lock b/flake.lock index 25475b1..a5257f0 100644 --- a/flake.lock +++ b/flake.lock @@ -5,11 +5,11 @@ "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1754487366, - "narHash": "sha256-pHYj8gUBapuUzKV/kN/tR3Zvqc7o6gdFB9XKXIp1SQ8=", + "lastModified": 1765835352, + "narHash": "sha256-XswHlK/Qtjasvhd1nOa1e8MgZ8GS//jBoTqWtrS1Giw=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "af66ad14b28a127c5c0f3bbb298218fc63528a18", + "rev": "a34fae9c08a15ad73f295041fec82323541400a9", "type": "github" }, "original": { @@ -18,13 +18,37 @@ "type": "github" } }, + "jrl-cmakemodules": { + "inputs": { + "flake-parts": [ + "flake-parts" + ], + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1766168186, + "narHash": "sha256-t4hNCMp9NxqHL/S3T9qO9W2YTo8SK9vjJJgzt3PMar8=", + "owner": "ahoarau", + "repo": "jrl-cmakemodules", + "rev": "68b07aaedacee648d77f5fb98962a87cb572130c", + "type": "github" + }, + "original": { + "owner": "ahoarau", + "ref": "jrl-next", + "repo": "jrl-cmakemodules", + "type": "github" + } + }, "nixpkgs": { "locked": { - "lastModified": 1756266583, - "narHash": "sha256-cr748nSmpfvnhqSXPiCfUPxRz2FJnvf/RjJGvFfaCsM=", - "owner": "NixOS", + "lastModified": 1766153546, + "narHash": "sha256-lIagbRYXHD1DClqzuqmkC3N/GOxZrwTh13eRtmNUyEg=", + "owner": "nim65s", "repo": "nixpkgs", - "rev": "8a6d5427d99ec71c64f0b93d45778c889005d9c2", + "rev": "a945047facf45faddcbfa2315207950334c62720", "type": "github" }, "original": { @@ -36,11 +60,11 @@ }, "nixpkgs-lib": { "locked": { - "lastModified": 1753579242, - "narHash": "sha256-zvaMGVn14/Zz8hnp4VWT9xVnhc8vuL3TStRqwk22biA=", + "lastModified": 1765674936, + "narHash": "sha256-k00uTP4JNfmejrCLJOwdObYC9jHRrr/5M/a/8L2EIdo=", "owner": "nix-community", "repo": "nixpkgs.lib", - "rev": "0f36c44e01a6129be94e3ade315a5883f0228a6e", + "rev": "2075416fcb47225d9b68ac469a5c4801a9c4dd85", "type": "github" }, "original": { @@ -52,6 +76,7 @@ "root": { "inputs": { "flake-parts": "flake-parts", + "jrl-cmakemodules": "jrl-cmakemodules", "nixpkgs": "nixpkgs" } } diff --git a/flake.nix b/flake.nix index 94a6735..e460740 100644 --- a/flake.nix +++ b/flake.nix @@ -4,30 +4,80 @@ inputs = { flake-parts.url = "github:hercules-ci/flake-parts"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + + # Test https://github.com/jrl-umi3218/jrl-cmakemodules/pull/798 + jrl-cmakemodules = { + url = "github:ahoarau/jrl-cmakemodules/jrl-next"; + inputs.flake-parts.follows = "flake-parts"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; outputs = inputs: - inputs.flake-parts.lib.mkFlake { inherit inputs; } { - systems = inputs.nixpkgs.lib.systems.flakeExposed; - perSystem = - { pkgs, self', ... }: - { - packages = { - default = self'.packages.nanoeigenpy; - nanoeigenpy = pkgs.python3Packages.nanoeigenpy.overrideAttrs (_: { - src = pkgs.lib.fileset.toSource { - root = ./.; - fileset = pkgs.lib.fileset.unions [ - ./CMakeLists.txt - ./include - ./package.xml - ./src - ./tests + inputs.flake-parts.lib.mkFlake { inherit inputs; } ( + { self, lib, ... }: + { + systems = inputs.nixpkgs.lib.systems.flakeExposed; + flake.overlays = { + default = final: prev: { + pythonPackagesExtensions = prev.pythonPackagesExtensions ++ [ + (python-final: python-prev: { + nanoeigenpy = python-prev.nanoeigenpy.overrideAttrs (old: { + cmakeFlags = (old.cmakeFlags or [ ]) ++ [ + "-DBUILD_TESTING=ON" + "-DBUILD_WITH_CHOLMOD_SUPPORT=OFF" + ]; + nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [ + python-final.pytest + ]; + # Don’t produce/require a separate doc output + outputs = [ "out" ]; + postPatch = ""; + postFixup = ""; + src = lib.fileset.toSource { + root = ./.; + fileset = lib.fileset.unions [ + ./cmake + ./CMakeLists.txt + ./include + ./package.xml + ./src + ./tests + ]; + }; + }); + }) + ]; + }; + }; + perSystem = + { + pkgs, + self', + system, + ... + }: + { + _module.args = { + pkgs = import inputs.nixpkgs { + inherit system; + overlays = [ + inputs.jrl-cmakemodules.overlays.default + self.overlays.default ]; }; - }); + }; + apps.default = { + type = "app"; + program = pkgs.python3.withPackages (_: [ self'.packages.default ]); + }; + packages = { + default = self'.packages.nanoeigenpy; + jrl-cmakemodules = pkgs.jrl-cmakemodules; + nanoeigenpy = pkgs.python3Packages.nanoeigenpy; + }; }; - }; - }; + } + ); } diff --git a/include/nanoeigenpy/decompositions/sparse/sparse-lu.hpp b/include/nanoeigenpy/decompositions/sparse/sparse-lu.hpp index 30d51b1..6f1aab7 100644 --- a/include/nanoeigenpy/decompositions/sparse/sparse-lu.hpp +++ b/include/nanoeigenpy/decompositions/sparse/sparse-lu.hpp @@ -63,8 +63,13 @@ void exposeSparseLU(nb::module_ m, const char *name) { using RealScalar = typename MatrixType::RealScalar; using StorageIndex = typename MatrixType::StorageIndex; using SCMatrix = typename Solver::SCMatrix; +#if EIGEN_VERSION_AT_LEAST(3, 5, 0) + using MappedSparseMatrix = typename Eigen::Map< + Eigen::SparseMatrix>; +#else using MappedSparseMatrix = typename Eigen::MappedSparseMatrix; +#endif using LType = Eigen::SparseLUMatrixLReturnType; using UType = Eigen::SparseLUMatrixUReturnType; diff --git a/include/nanoeigenpy/fwd.hpp b/include/nanoeigenpy/fwd.hpp index 9546110..3a08d4e 100644 --- a/include/nanoeigenpy/fwd.hpp +++ b/include/nanoeigenpy/fwd.hpp @@ -7,43 +7,22 @@ #include "nanoeigenpy/utils/helpers.hpp" #include +#ifdef NANOEIGENPY_HAS_CHOLMOD +#include +#include +#endif + +#ifdef NANOEIGENPY_HAS_ACCELERATE +#include +#include +#endif + #include #include #include #include #include -#if defined(__clang__) -#define NANOEIGENPY_CLANG_COMPILER -#elif defined(__GNUC__) -#define NANOEIGENPY_GCC_COMPILER -#elif defined(_MSC_VER) -#define NANOEIGENPY_MSVC_COMPILER -#endif - -#if __has_include() -#define NANOEIGENPY_HAS_CHOLMOD -#endif - -#if __has_include() -#define NANOEIGENPY_HAS_ACCELERATE -#endif - -#if (__cplusplus >= 202002L || (defined(_MSVC_LANG) && _MSVC_LANG >= 202002L)) -#define NANOEIGENPY_WITH_CXX20_SUPPORT -#endif - -#define NANOEIGENPY_UNUSED_TYPE(Type) (void)(Type*)(NULL) - -#define NANOEIGENPY_MAKE_TYPEDEFS(Type, Options, TypeSuffix, Size, SizeSuffix) \ - /** \ingroup matrixtypedefs */ \ - typedef Eigen::Matrix \ - Matrix##SizeSuffix##TypeSuffix; \ - /** \ingroup matrixtypedefs */ \ - typedef Eigen::Matrix Vector##SizeSuffix##TypeSuffix; \ - /** \ingroup matrixtypedefs */ \ - typedef Eigen::Matrix RowVector##SizeSuffix##TypeSuffix; - namespace nanoeigenpy { namespace nb = nanobind; } // namespace nanoeigenpy diff --git a/pixi.lock b/pixi.lock index c04a793..2150488 100644 --- a/pixi.lock +++ b/pixi.lock @@ -32,7 +32,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-14.3.0-h2185e75_16.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_linux-64-14.3.0-hdb5f4f1_15.conda - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-4.18.0-he073ed8_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-4.18.0-he073ed8_9.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-default_hbd61a6d_104.conda @@ -83,11 +83,11 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/rhash-1.4.6-hb9d3cd8_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/scipy-1.16.3-py314he7377e1_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_9.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda osx-64: - conda: https://conda.anaconda.org/conda-forge/osx-64/_openmp_mutex-4.5-7_kmp_llvm.conda @@ -174,7 +174,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-hf689a15_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda osx-arm64: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/_openmp_mutex-4.5-7_kmp_llvm.conda @@ -260,7 +260,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda win-64: - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_8.conda @@ -310,7 +310,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h2c6b04d_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h2b53caa_33.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_33.conda @@ -352,7 +352,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-14.3.0-h2185e75_16.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_linux-64-14.3.0-hdb5f4f1_15.conda - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-4.18.0-he073ed8_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-4.18.0-he073ed8_9.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-default_hbd61a6d_104.conda @@ -421,11 +421,11 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/rhash-1.4.6-hb9d3cd8_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/scipy-1.16.3-py314he7377e1_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/suitesparse-7.10.1-h5b2951e_7100101.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_9.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda osx-64: - conda: https://conda.anaconda.org/conda-forge/osx-64/_openmp_mutex-4.5-7_kmp_llvm.conda @@ -531,7 +531,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-hf689a15_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda osx-arm64: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/_openmp_mutex-4.5-7_kmp_llvm.conda @@ -636,7 +636,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda win-64: - conda: https://conda.anaconda.org/conda-forge/win-64/_openmp_mutex-4.5-2_gnu.conda @@ -708,7 +708,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h2c6b04d_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h2b53caa_33.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_33.conda @@ -799,7 +799,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h2c6b04d_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h2b53caa_33.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_33.conda @@ -841,7 +841,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-14.3.0-h2185e75_16.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_linux-64-14.3.0-hdb5f4f1_15.conda - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-4.18.0-he073ed8_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-4.18.0-he073ed8_9.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-default_hbd61a6d_104.conda @@ -910,11 +910,11 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/rhash-1.4.6-hb9d3cd8_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/scipy-1.15.2-py310h1d65ade_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/suitesparse-7.10.1-h5b2951e_7100101.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_9.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda osx-64: - conda: https://conda.anaconda.org/conda-forge/osx-64/_openmp_mutex-4.5-7_kmp_llvm.conda @@ -1019,7 +1019,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-hf689a15_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda osx-arm64: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/_openmp_mutex-4.5-7_kmp_llvm.conda @@ -1123,7 +1123,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda win-64: - conda: https://conda.anaconda.org/conda-forge/win-64/_openmp_mutex-4.5-2_gnu.conda @@ -1194,7 +1194,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h2c6b04d_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h2b53caa_33.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_33.conda @@ -1236,7 +1236,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-14.3.0-h2185e75_16.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_linux-64-14.3.0-hdb5f4f1_15.conda - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-4.18.0-he073ed8_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-4.18.0-he073ed8_9.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-default_hbd61a6d_104.conda @@ -1305,11 +1305,11 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/rhash-1.4.6-hb9d3cd8_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/scipy-1.16.3-py314he7377e1_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/suitesparse-7.10.1-h5b2951e_7100101.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_9.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda osx-64: - conda: https://conda.anaconda.org/conda-forge/osx-64/_openmp_mutex-4.5-7_kmp_llvm.conda @@ -1415,7 +1415,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-hf689a15_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda osx-arm64: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/_openmp_mutex-4.5-7_kmp_llvm.conda @@ -1520,7 +1520,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda win-64: - conda: https://conda.anaconda.org/conda-forge/win-64/_openmp_mutex-4.5-2_gnu.conda @@ -1592,7 +1592,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h2c6b04d_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h2b53caa_33.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_33.conda @@ -1638,7 +1638,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-14.3.0-h2185e75_16.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_linux-64-14.3.0-hdb5f4f1_15.conda - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-4.18.0-he073ed8_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-4.18.0-he073ed8_9.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-default_hbd61a6d_104.conda @@ -1695,11 +1695,11 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/rhash-1.4.6-hb9d3cd8_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/scipy-1.16.3-py314he7377e1_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_9.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda default: channels: @@ -1733,7 +1733,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-14.3.0-h2185e75_16.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_linux-64-14.3.0-hdb5f4f1_15.conda - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-4.18.0-he073ed8_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-4.18.0-he073ed8_9.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-default_hbd61a6d_104.conda @@ -1784,11 +1784,11 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/rhash-1.4.6-hb9d3cd8_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/scipy-1.16.3-py314he7377e1_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_9.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda osx-64: - conda: https://conda.anaconda.org/conda-forge/osx-64/_openmp_mutex-4.5-7_kmp_llvm.conda @@ -1875,7 +1875,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-hf689a15_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda osx-arm64: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/_openmp_mutex-4.5-7_kmp_llvm.conda @@ -1961,7 +1961,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda win-64: - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_8.conda @@ -2011,7 +2011,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h2c6b04d_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h2b53caa_33.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_33.conda @@ -2038,10 +2038,12 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.11.12-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ccache-4.11.3-h80c52d3_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cmake-4.2.1-hc85cc9f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/conda-gcc-specs-14.3.0-he8ccf15_16.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cxx-compiler-1.11.0-hfcd1e18_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/doxygen-1.13.2-h8e693c7_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/eigen-3.4.0-h171cf75_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/gcc-14.3.0-h0dff253_16.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-14.3.0-he8b2097_16.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/gcc_linux-64-14.3.0-h298d278_15.conda @@ -2049,7 +2051,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/gxx-14.3.0-h76987e4_16.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-14.3.0-h2185e75_16.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_linux-64-14.3.0-hdb5f4f1_15.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-4.18.0-he073ed8_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-4.18.0-he073ed8_9.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-default_hbd61a6d_104.conda @@ -2089,17 +2092,23 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/ninja-1.13.2-h171cf75_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.5-py314h2b28147_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.0-h26f9b46_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.47-haa7fec5_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/perl-5.32.1-7_hd590300_perl5.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.0.2-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.14.2-h32b2ec7_100_cp314.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/rhash-1.4.6-hb9d3cd8_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/scipy-1.16.3-py314he7377e1_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_9.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.13.3-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda osx-64: - conda: https://conda.anaconda.org/conda-forge/osx-64/_openmp_mutex-4.5-7_kmp_llvm.conda @@ -2119,13 +2128,16 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/clangxx_impl_osx-64-19.1.7-hb295874_27.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/clangxx_osx-64-19.1.7-h7e5c614_27.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/cmake-4.2.1-h29fc008_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/compiler-rt-19.1.7-he914875_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/compiler-rt_osx-64-19.1.7-h138dee1_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/cxx-compiler-1.11.0-h307afc9_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/doxygen-1.13.2-h27064b9_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/eigen-3.4.0-hfc0b2d5_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/git-2.52.0-pl5321hfcb5ae3_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/icu-75.1-h120a0e1_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/krb5-1.21.3-h37d8d59_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/ld64-956.6-llvm19_1_hc3792c1_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/ld64_osx-64-956.6-llvm19_1_h466f870_1.conda @@ -2166,8 +2178,12 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/ninja-1.13.2-hfc0b2d5_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/numpy-2.3.5-py314hf08249b_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.6.0-h230baf5_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/pcre2-10.47-h13923f0_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/perl-5.32.1-7_h10d778d_perl5.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.0.2-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.14.2-hf88997e_100_cp314.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.2-h7cca4af_2.conda @@ -2177,8 +2193,10 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/sigtool-0.1.3-h88f4db0_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/osx-64/tapi-1600.0.11.8-h8d8e812_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-hf689a15_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.13.3-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda osx-arm64: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/_openmp_mutex-4.5-7_kmp_llvm.conda @@ -2198,12 +2216,15 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clangxx_impl_osx-arm64-19.1.7-h276745f_27.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clangxx_osx-arm64-19.1.7-h07b0088_27.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cmake-4.2.1-h54ad630_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/compiler-rt-19.1.7-h855ad52_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/compiler-rt_osx-arm64-19.1.7-he32a8d3_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cxx-compiler-1.11.0-h88570a1_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/doxygen-1.13.2-h493aca8_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/eigen-3.4.0-h49c215f_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/git-2.52.0-pl5321h8012a55_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.21.3-h237132a_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ld64-956.6-llvm19_1_he86490a_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ld64_osx-arm64-956.6-llvm19_1_h6922315_1.conda @@ -2244,8 +2265,12 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ninja-1.13.2-h49c215f_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/numpy-2.3.5-py314h5b5928d_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.0-h5503f6c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pcre2-10.47-h30297fc_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/perl-5.32.1-7_h4614cfb_perl5.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.0.2-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.14.2-h40d2674_100_cp314.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h1d1bf99_2.conda @@ -2255,18 +2280,23 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/sigtool-0.1.3-h44b9a77_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tapi-1600.0.11.8-h997e182_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.13.3-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda win-64: - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_8.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.11.12-h4c7d964_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ccache-4.11.3-h12b022e_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cmake-4.2.1-hdcbee5b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cxx-compiler-1.11.0-h1c1089f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/doxygen-1.13.2-hbf3f430_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/eigen-3.4.0-h477610d_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/git-2.52.0-h57928b3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.21.3-hdf4eb48_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-4_hf2e6a31_mkl.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-4_h2a3cdd5_mkl.conda @@ -2292,13 +2322,19 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/ninja-1.13.2-h477610d_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/numpy-2.3.5-py314h06c3c77_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.0-h725018a_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.0.2-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.14.2-h4b44e0e_100_cp314.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda - conda: https://conda.anaconda.org/conda-forge/win-64/scipy-1.16.3-py314h5798d8a_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2022.3.0-hd094cb3_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h2c6b04d_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.13.3-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h2b53caa_33.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_33.conda @@ -2339,7 +2375,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-14.3.0-h2185e75_16.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_linux-64-14.3.0-hdb5f4f1_15.conda - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-4.18.0-he073ed8_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-4.18.0-he073ed8_9.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-default_hbd61a6d_104.conda @@ -2390,11 +2426,11 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/rhash-1.4.6-hb9d3cd8_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/scipy-1.15.2-py310h1d65ade_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_9.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda osx-64: - conda: https://conda.anaconda.org/conda-forge/osx-64/_openmp_mutex-4.5-7_kmp_llvm.conda @@ -2480,7 +2516,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-hf689a15_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda osx-arm64: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/_openmp_mutex-4.5-7_kmp_llvm.conda @@ -2565,7 +2601,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda win-64: - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_8.conda @@ -2614,7 +2650,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h2c6b04d_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h2b53caa_33.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_33.conda @@ -2672,7 +2708,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/rhash-1.4.6-hb9d3cd8_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda - conda: . build: hb0f4dca_0 @@ -2714,7 +2750,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.2-h7cca4af_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/rhash-1.4.6-h6e16a3a_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-hf689a15_3.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda - conda: . build: h0dc7051_0 @@ -2755,7 +2791,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h1d1bf99_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/rhash-1.4.6-h5505292_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_3.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda - conda: . build: h60d57d3_0 @@ -2790,7 +2826,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2022.3.0-hd094cb3_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h2c6b04d_3.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h2b53caa_33.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_33.conda @@ -3879,15 +3915,15 @@ packages: license_family: MIT size: 13387 timestamp: 1760831448842 -- conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-4.18.0-he073ed8_8.conda - sha256: 305c22a251db227679343fd73bfde121e555d466af86e537847f4c8b9436be0d - md5: ff007ab0f0fdc53d245972bba8a6d40c +- conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-4.18.0-he073ed8_9.conda + sha256: 41557eeadf641de6aeae49486cef30d02a6912d8da98585d687894afd65b356a + md5: 86d9cba083cd041bfbf242a01a7a1999 constrains: - sysroot_linux-64 ==2.28 license: LGPL-2.0-or-later AND LGPL-2.0-or-later WITH exceptions AND GPL-2.0-or-later license_family: GPL - size: 1272697 - timestamp: 1752669126073 + size: 1278712 + timestamp: 1765578681495 - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda sha256: 0960d06048a7185d3542d850986d807c6e37ca2e644342dd0c72feefcf26c2a4 md5: b38117a3c920364aff79f870c984b4a3 @@ -7402,17 +7438,17 @@ packages: license: LGPL-2.1-or-later AND BSD-3-Clause AND GPL-2.0-or-later AND Apache-2.0 size: 12345 timestamp: 1742288893865 -- conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_8.conda - sha256: 0053c17ffbd9f8af1a7f864995d70121c292e317804120be4667f37c92805426 - md5: 1bad93f0aa428d618875ef3a588a889e +- conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_9.conda + sha256: c47299fe37aebb0fcf674b3be588e67e4afb86225be4b0d452c7eb75c086b851 + md5: 13dc3adbc692664cd3beabd216434749 depends: - __glibc >=2.28 - - kernel-headers_linux-64 4.18.0 he073ed8_8 + - kernel-headers_linux-64 4.18.0 he073ed8_9 - tzdata license: LGPL-2.0-or-later AND LGPL-2.0-or-later WITH exceptions AND GPL-2.0-or-later license_family: GPL - size: 24210909 - timestamp: 1752669140965 + size: 24008591 + timestamp: 1765578833462 - conda: https://conda.anaconda.org/conda-forge/osx-64/tapi-1600.0.11.8-h8d8e812_0.conda sha256: 2602632f7923fd59042a897bfb22f050d78f2b5960d53565eae5fa6a79308caa md5: aae272355bc3f038e403130a5f6f5495 @@ -7518,12 +7554,12 @@ packages: license_family: PSF size: 51692 timestamp: 1756220668932 -- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - sha256: 5aaa366385d716557e365f0a4e9c3fca43ba196872abbbe3d56bb610d131e192 - md5: 4222072737ccff51314b5ece9c7d6f5a +- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda + sha256: 865716d3e2ccaca1218462645830d2370ab075a9a118c238728e1231a234bc6c + md5: e4e8496b68cf5f25e76fbe67f3856550 license: LicenseRef-Public-Domain - size: 122968 - timestamp: 1742727099393 + size: 119010 + timestamp: 1765580300078 - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda sha256: 3005729dce6f3d3f5ec91dfc49fc75a0095f9cd23bab49efb899657297ac91a5 md5: 71b24316859acd00bdb8b38f5e2ce328 diff --git a/pixi.toml b/pixi.toml index 4142b97..2359319 100644 --- a/pixi.toml +++ b/pixi.toml @@ -22,6 +22,12 @@ extra-args = [ "-DBUILD_WITH_ACCELERATE_SUPPORT=OFF", ] +# Override python install dir for Conda on Windows +[package.build.target.win-64.config] +extra-args = [ + "-Dnanoeigenpy_PYTHON_INSTALL_DIR=%PREFIX%/Lib/site-packages", +] + [package.host-dependencies] nanobind = ">=2.5.0" python = ">=3.10" @@ -43,6 +49,7 @@ python = ">=3.10" eigen = ">=3.4" numpy = ">=1.22" scipy = ">=1.10.0" +pytest = ">=9.0.2,<10" # nanobind need to use OSX SDK >= 10.13. # But default version setup by rattler is 10.9 @@ -77,13 +84,42 @@ configure = { cmd = [ "build", "-S", ".", + "-DJRL_CMAKEMODULES_SOURCE_DIR=$JRL_CMAKEMODULES_SOURCE_DIR", "-DCMAKE_INSTALL_PREFIX=$CONDA_PREFIX", + "-DBUILD_TESTING=ON", "-DCMAKE_BUILD_TYPE=$NANOEIGENPY_BUILD_TYPE", "-DBUILD_WITH_CHOLMOD_SUPPORT=$NANOEIGENPY_CHOLMOD_SUPPORT", "-DBUILD_WITH_ACCELERATE_SUPPORT=$NANOEIGENPY_ACCELERATE_SUPPORT", ] } -build = { cmd = "cmake --build build --target all", depends-on = ["configure"] } +build = { cmd = "cmake --build build", depends-on = ["configure"] } clean = { cmd = "rm -rf build" } +install = { cmd = "cmake --install build", depends-on = ["build"] } +test = { cmd = "ctest --output-on-failure --test-dir build", depends-on = [ + "build", +] } + +test-import-python = { depends-on = [ + "install", +], cmd = [ + "python", + "-c", + "import nanoeigenpy; print(nanoeigenpy.__version__)", +] } +_test-packaging-configure = { cmd = [ + "cmake", + "-G", + "Ninja", + "-S", + "tests/packaging/cmake", + "-B", + "build/test-packaging", +], depends-on = [ + "install", +] } +_test-packaging-build = { cmd = "cmake --build build/test-packaging", depends-on = [ + "_test-packaging-configure", +] } +test-packaging = { depends-on = ["_test-packaging-build"] } # Increment the version number with NANOEIGENPY_VERSION variable [feature.new-version.dependencies] @@ -135,8 +171,8 @@ dependencies = { clangxx = "*", lld = "*" } # Absolute path is needed to avoid using system clang-cl [feature.clang-cl.activation.env] -CC = "%CONDA_PREFIX%\\Library\\bin\\clang-cl" -CXX = "%CONDA_PREFIX%\\Library\\bin\\clang-cl" +CC = "%CONDA_PREFIX%/Library/bin/clang-cl" +CXX = "%CONDA_PREFIX%/Library/bin/clang-cl" # Use clang on GNU/Linux [feature.clang] @@ -148,8 +184,8 @@ dependencies = { clangxx = "*", lld = "*" } dependencies = { nanoeigenpy = { path = "." }, cmake = ">=3.22", python = "*" } [feature.test-pixi-build.tasks] -test-cmake = "cmake -S tests/packaging/pixi_build -B build_test_pixi_build" -test-python = "python -c 'import nanoeigenpy'" +test-cmake = "cmake -S tests/packaging/cmake -B build/test_pixi_build" +test-python = "python -c 'import nanoeigenpy; print(nanoeigenpy.__version__)'" test = { depends-on = ["test-cmake", "test-python"] } [environments] diff --git a/src/internal.h b/src/internal.h deleted file mode 100644 index ea9cbe9..0000000 --- a/src/internal.h +++ /dev/null @@ -1,9 +0,0 @@ -#include -#include - -namespace nb = nanobind; - -using Scalar = double; -static constexpr int Options = Eigen::ColMajor; -using Matrix = Eigen::Matrix; -using Vector = Eigen::Matrix; diff --git a/src/module.cpp b/src/module.cpp index 3dfd59a..2d12755 100644 --- a/src/module.cpp +++ b/src/module.cpp @@ -2,13 +2,19 @@ #include +#include "nanoeigenpy/fwd.hpp" #include "nanoeigenpy/decompositions.hpp" #include "nanoeigenpy/geometry.hpp" #include "nanoeigenpy/solvers.hpp" #include "nanoeigenpy/constants.hpp" #include "nanoeigenpy/utils/is-approx.hpp" -#include "./internal.h" +namespace nb = nanobind; + +using Scalar = double; +static constexpr int Options = Eigen::ColMajor; +using Matrix = Eigen::Matrix; +using Vector = Eigen::Matrix; using namespace nanoeigenpy; @@ -31,9 +37,13 @@ using SparseQR = Eigen::SparseQR>; using SparseLU = Eigen::SparseLU; using SCMatrix = typename SparseLU::SCMatrix; using StorageIndex = typename Matrix::StorageIndex; +#if EIGEN_VERSION_AT_LEAST(3, 5, 0) +using MappedSparseMatrix = typename Eigen::Map< + Eigen::SparseMatrix>; +#else using MappedSparseMatrix = - typename Eigen::MappedSparseMatrix; - + typename Eigen::MappedSparseMatrix; +#endif NB_MAKE_OPAQUE(ColPivHhJacobiSVD) NB_MAKE_OPAQUE(FullPivHhJacobiSVD) NB_MAKE_OPAQUE(HhJacobiSVD) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 18cfe81..06b5446 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,108 +1,58 @@ -# Copyright 2025 INRIA - -# Create a shared nanobind library for testing -set(NANOBIND_TESTING_TARGET nanobind-testing) -nanobind_build_library(${NANOBIND_TESTING_TARGET} SHARED) - -# On Win32, shared DLL libs are sent to RUNTIME_OUTPUT_DIRECTORY, *but* -# we really need to send it to the lib dir so -if(WIN32) - set_target_properties( - ${NANOBIND_TESTING_TARGET} - PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib - ) -endif() - -# Add a C++ extension module for tests -function(add_tests_cpp_extension name) - set(filename ${name}.cpp) - add_library(${name} MODULE ${filename}) - target_link_libraries( - ${name} - PRIVATE ${NANOBIND_TESTING_TARGET} nanoeigenpy_headers - ) - # Use nanobind low-level interface to set properties - nanobind_set_visibility(${name}) - nanobind_strip(${name}) - nanobind_extension(${name}) - nanobind_compile_options(${name}) - nanobind_link_options(${name}) - - add_dependencies(build_tests ${name}) +nanobind_add_module(quaternion + NB_STATIC LTO NB_SUPPRESS_WARNINGS + quaternion.cpp +) +target_link_libraries(quaternion PRIVATE Eigen3::Eigen) +jrl_target_set_output_directory(quaternion OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/test-quaternion) +function(nanoeigenpy_add_test name) + set(test_name "nanoeigenpy: ${name}") add_test( - NAME "${PROJECT_NAME}-import-${name}" - COMMAND ${Python_EXECUTABLE} -c "import ${name}" - WORKING_DIRECTORY $ + NAME ${test_name} + COMMAND $ ${CMAKE_CURRENT_SOURCE_DIR}/test_${name}.py + ) + set_property( + TEST ${test_name} + PROPERTY + ENVIRONMENT_MODIFICATION + "PYTHONPATH=path_list_prepend:$" + "PYTHONPATH=path_list_prepend:$" ) endfunction() -# Add Python test module -function(add_tests_py_module name) - set(filename tests/${name}.py) - set(test_target "${PROJECT_NAME}-${name}") - string(REPLACE "_" "-" test_target ${test_target}) - set(PYTHON_EXECUTABLE ${Python_EXECUTABLE}) - ADD_PYTHON_UNIT_TEST(${test_target} ${filename} "lib") - unset(PYTHON_EXECUTABLE) - set_tests_properties(${test_target} PROPERTIES DEPENDS nanoeigenpy) -endfunction() - -add_dependencies(build_tests nanoeigenpy) - -add_test( - NAME "${PROJECT_NAME}-import-extension" - COMMAND ${Python_EXECUTABLE} -c "import ${PROJECT_NAME}" - WORKING_DIRECTORY $ -) - -add_tests_cpp_extension(quaternion) - -set( - TEST_NAMES - test_eigen_solver - test_complex_eigen_solver - test_generalized_eigen_solver - test_self_adjoint_eigen_solver - test_generalized_self_adjoint_eigen_solver - test_real_schur - test_complex_schur - test_hessenberg_decomposition - test_real_qz - test_tridiagonalization - test_bdcsvd - test_jacobi_svd - test_full_piv_lu - test_partial_piv_lu - test_ldlt - test_llt - test_qr - test_simplicial_llt - test_sparse_lu - test_sparse_qr - test_geometry - test_iterative_solvers - test_permutation_matrix - test_incomplete_lut - test_incomplete_cholesky -) +nanoeigenpy_add_test(bdcsvd) +nanoeigenpy_add_test(complex_eigen_solver) +nanoeigenpy_add_test(complex_schur) +nanoeigenpy_add_test(eigen_solver) +nanoeigenpy_add_test(full_piv_lu) +nanoeigenpy_add_test(generalized_eigen_solver) +nanoeigenpy_add_test(generalized_self_adjoint_eigen_solver) +nanoeigenpy_add_test(geometry) +nanoeigenpy_add_test(hessenberg_decomposition) +nanoeigenpy_add_test(import_extension) +nanoeigenpy_add_test(incomplete_cholesky) +nanoeigenpy_add_test(incomplete_lut) +nanoeigenpy_add_test(iterative_solvers) +nanoeigenpy_add_test(jacobi_svd) +nanoeigenpy_add_test(ldlt) +nanoeigenpy_add_test(llt) +nanoeigenpy_add_test(partial_piv_lu) +nanoeigenpy_add_test(permutation_matrix) +nanoeigenpy_add_test(qr) +nanoeigenpy_add_test(real_qz) +nanoeigenpy_add_test(real_schur) +nanoeigenpy_add_test(self_adjoint_eigen_solver) +nanoeigenpy_add_test(simplicial_llt) +nanoeigenpy_add_test(sparse_lu) +nanoeigenpy_add_test(sparse_qr) +nanoeigenpy_add_test(tridiagonalization) if(BUILD_WITH_CHOLMOD_SUPPORT) - list( - APPEND - TEST_NAMES - test_cholmod_simplicial_ldlt - test_cholmod_simplicial_llt - test_cholmod_supernodal_llt - ) -endif(BUILD_WITH_CHOLMOD_SUPPORT) - -foreach(test_name ${TEST_NAMES}) - message(STATUS "Adding Python test ${test_name}") - add_tests_py_module(${test_name}) -endforeach() + nanoeigenpy_add_test(cholmod_simplicial_ldlt) + nanoeigenpy_add_test(cholmod_simplicial_llt) + nanoeigenpy_add_test(cholmod_supernodal_llt) +endif() if(BUILD_WITH_ACCELERATE_SUPPORT) - message(STATUS "Adding Python test test_accelerate") - add_tests_py_module(test_accelerate) -endif(BUILD_WITH_ACCELERATE_SUPPORT) + nanoeigenpy_add_test(accelerate) +endif() diff --git a/tests/packaging/pixi_build/CMakeLists.txt b/tests/packaging/cmake/CMakeLists.txt similarity index 61% rename from tests/packaging/pixi_build/CMakeLists.txt rename to tests/packaging/cmake/CMakeLists.txt index 46e4847..6971366 100644 --- a/tests/packaging/pixi_build/CMakeLists.txt +++ b/tests/packaging/cmake/CMakeLists.txt @@ -1,3 +1,4 @@ cmake_minimum_required(VERSION 3.22) project(test_pixi_build) find_package(nanoeigenpy REQUIRED) +message(STATUS "nanoeigenpy found: ${nanoeigenpy_VERSION}") diff --git a/tests/test_accelerate.py b/tests/test_accelerate.py index d21d46d..f02eebd 100644 --- a/tests/test_accelerate.py +++ b/tests/test_accelerate.py @@ -1,11 +1,23 @@ import nanoeigenpy import numpy as np from scipy.sparse import csc_matrix +import pytest rng = np.random.default_rng() -def test(SolverType: type): +@pytest.mark.parametrize( + "SolverType", + [ + nanoeigenpy.AccelerateLLT, + nanoeigenpy.AccelerateLDLT, + nanoeigenpy.AccelerateLDLTUnpivoted, + nanoeigenpy.AccelerateLDLTSBK, + nanoeigenpy.AccelerateLDLTTPP, + nanoeigenpy.AccelerateQR, + ], +) +def test_accelerate_solver(SolverType): dim = 100 A = rng.random((dim, dim)) A = (A + A.T) * 0.5 + np.diag(10.0 + rng.random(dim)) @@ -25,11 +37,3 @@ def test(SolverType: type): llt.analyzePattern(A) llt.factorize(A) - - -test(nanoeigenpy.AccelerateLLT) -test(nanoeigenpy.AccelerateLDLT) -test(nanoeigenpy.AccelerateLDLTUnpivoted) -test(nanoeigenpy.AccelerateLDLTSBK) -test(nanoeigenpy.AccelerateLDLTTPP) -test(nanoeigenpy.AccelerateQR) diff --git a/tests/test_cholmod_simplicial_ldlt.py b/tests/test_cholmod_simplicial_ldlt.py index cb1b050..3532467 100644 --- a/tests/test_cholmod_simplicial_ldlt.py +++ b/tests/test_cholmod_simplicial_ldlt.py @@ -1,24 +1,25 @@ +import nanoeigenpy import numpy as np from scipy.sparse import csc_matrix -import nanoeigenpy -dim = 100 -rng = np.random.default_rng() -A = rng.random((dim, dim)) -A = (A + A.T) * 0.5 + np.diag(10.0 + rng.random(dim)) +def test_cholmod_simplicial_ldlt(): + dim = 100 + rng = np.random.default_rng() + A = rng.random((dim, dim)) + A = (A + A.T) * 0.5 + np.diag(10.0 + rng.random(dim)) -A = csc_matrix(A) + A = csc_matrix(A) -llt = nanoeigenpy.CholmodSimplicialLDLT(A) + llt = nanoeigenpy.CholmodSimplicialLDLT(A) -assert llt.info() == nanoeigenpy.ComputationInfo.Success + assert llt.info() == nanoeigenpy.ComputationInfo.Success -X = rng.random((dim, 20)) -B = A.dot(X) -X_est = llt.solve(B) -assert nanoeigenpy.is_approx(X, X_est) -assert nanoeigenpy.is_approx(A.dot(X_est), B) + X = rng.random((dim, 20)) + B = A.dot(X) + X_est = llt.solve(B) + assert nanoeigenpy.is_approx(X, X_est) + assert nanoeigenpy.is_approx(A.dot(X_est), B) -llt.analyzePattern(A) -llt.factorize(A) + llt.analyzePattern(A) + llt.factorize(A) diff --git a/tests/test_cholmod_simplicial_llt.py b/tests/test_cholmod_simplicial_llt.py index 763cfaa..2b8137c 100644 --- a/tests/test_cholmod_simplicial_llt.py +++ b/tests/test_cholmod_simplicial_llt.py @@ -1,25 +1,26 @@ +import nanoeigenpy import numpy as np from scipy.sparse import csc_matrix -import nanoeigenpy -dim = 100 -rng = np.random.default_rng() +def test_cholmod_simplicial_llt(): + dim = 100 + rng = np.random.default_rng() -A = rng.random((dim, dim)) -A = (A + A.T) * 0.5 + np.diag(10.0 + rng.random(dim)) + A = rng.random((dim, dim)) + A = (A + A.T) * 0.5 + np.diag(10.0 + rng.random(dim)) -A = csc_matrix(A) + A = csc_matrix(A) -llt = nanoeigenpy.CholmodSimplicialLLT(A) + llt = nanoeigenpy.CholmodSimplicialLLT(A) -assert llt.info() == nanoeigenpy.ComputationInfo.Success + assert llt.info() == nanoeigenpy.ComputationInfo.Success -X = rng.random((dim, 20)) -B = A.dot(X) -X_est = llt.solve(B) -assert nanoeigenpy.is_approx(X, X_est) -assert nanoeigenpy.is_approx(A.dot(X_est), B) + X = rng.random((dim, 20)) + B = A.dot(X) + X_est = llt.solve(B) + assert nanoeigenpy.is_approx(X, X_est) + assert nanoeigenpy.is_approx(A.dot(X_est), B) -llt.analyzePattern(A) -llt.factorize(A) + llt.analyzePattern(A) + llt.factorize(A) diff --git a/tests/test_cholmod_supernodal_llt.py b/tests/test_cholmod_supernodal_llt.py index 15de556..c938531 100644 --- a/tests/test_cholmod_supernodal_llt.py +++ b/tests/test_cholmod_supernodal_llt.py @@ -1,25 +1,26 @@ +import nanoeigenpy import numpy as np from scipy.sparse import csc_matrix -import nanoeigenpy -dim = 100 -rng = np.random.default_rng() +def test_cholmod_supernodal_llt(): + dim = 100 + rng = np.random.default_rng() -A = rng.random((dim, dim)) -A = (A + A.T) * 0.5 + np.diag(10.0 + rng.random(dim)) + A = rng.random((dim, dim)) + A = (A + A.T) * 0.5 + np.diag(10.0 + rng.random(dim)) -A = csc_matrix(A) + A = csc_matrix(A) -llt = nanoeigenpy.CholmodSupernodalLLT(A) + llt = nanoeigenpy.CholmodSupernodalLLT(A) -assert llt.info() == nanoeigenpy.ComputationInfo.Success + assert llt.info() == nanoeigenpy.ComputationInfo.Success -X = rng.random((dim, 20)) -B = A.dot(X) -X_est = llt.solve(B) -assert nanoeigenpy.is_approx(X, X_est) -assert nanoeigenpy.is_approx(A.dot(X_est), B) + X = rng.random((dim, 20)) + B = A.dot(X) + X_est = llt.solve(B) + assert nanoeigenpy.is_approx(X, X_est) + assert nanoeigenpy.is_approx(A.dot(X_est), B) -llt.analyzePattern(A) -llt.factorize(A) + llt.analyzePattern(A) + llt.factorize(A) diff --git a/tests/test_complex_eigen_solver.py b/tests/test_complex_eigen_solver.py index df6527a..3b3d85a 100644 --- a/tests/test_complex_eigen_solver.py +++ b/tests/test_complex_eigen_solver.py @@ -1,32 +1,34 @@ import nanoeigenpy import numpy as np -dim = 100 -rng = np.random.default_rng() -A = rng.random((dim, dim)) -es = nanoeigenpy.ComplexEigenSolver(A) -assert es.info() == nanoeigenpy.ComputationInfo.Success +def test_complex_eigen_solver(): + dim = 100 + rng = np.random.default_rng() + A = rng.random((dim, dim)) -V = es.eigenvectors() -D = es.eigenvalues() -assert V.shape == (dim, dim) -assert D.shape == (dim,) + es = nanoeigenpy.ComplexEigenSolver(A) + assert es.info() == nanoeigenpy.ComputationInfo.Success -AV = A @ V -VD = V @ np.diag(D) -assert nanoeigenpy.is_approx(AV.real, VD.real) -assert nanoeigenpy.is_approx(AV.imag, VD.imag) + V = es.eigenvectors() + D = es.eigenvalues() + assert V.shape == (dim, dim) + assert D.shape == (dim,) -trace_A = np.trace(A) -trace_D = np.sum(D) -assert abs(trace_A - trace_D.real) < 1e-10 -assert abs(trace_D.imag) < 1e-10 + AV = A @ V + VD = V @ np.diag(D) + assert nanoeigenpy.is_approx(AV.real, VD.real) + assert nanoeigenpy.is_approx(AV.imag, VD.imag) -ces5 = nanoeigenpy.ComplexEigenSolver(A) -ces6 = nanoeigenpy.ComplexEigenSolver(A) -id5 = ces5.id() -id6 = ces6.id() -assert id5 != id6 -assert id5 == ces5.id() -assert id6 == ces6.id() + trace_A = np.trace(A) + trace_D = np.sum(D) + assert abs(trace_A - trace_D.real) < 1e-10 + assert abs(trace_D.imag) < 1e-10 + + ces5 = nanoeigenpy.ComplexEigenSolver(A) + ces6 = nanoeigenpy.ComplexEigenSolver(A) + id5 = ces5.id() + id6 = ces6.id() + assert id5 != id6 + assert id5 == ces5.id() + assert id6 == ces6.id() diff --git a/tests/test_complex_schur.py b/tests/test_complex_schur.py index da1acb9..55a671b 100644 --- a/tests/test_complex_schur.py +++ b/tests/test_complex_schur.py @@ -1,49 +1,51 @@ import nanoeigenpy import numpy as np -dim = 100 -rng = np.random.default_rng() -A = rng.random((dim, dim)) -cs = nanoeigenpy.ComplexSchur(A) -assert cs.info() == nanoeigenpy.ComputationInfo.Success +def test_complex_schur(): + dim = 100 + rng = np.random.default_rng() + A = rng.random((dim, dim)) -U = cs.matrixU() -T = cs.matrixT() + cs = nanoeigenpy.ComplexSchur(A) + assert cs.info() == nanoeigenpy.ComputationInfo.Success -A_complex = A.astype(complex) -assert nanoeigenpy.is_approx(A_complex, U @ T @ U.conj().T) -assert nanoeigenpy.is_approx(U @ U.conj().T, np.eye(dim)) + U = cs.matrixU() + T = cs.matrixT() -for row in range(1, dim): - for col in range(row): - assert abs(T[row, col]) < 1e-12 + A_complex = A.astype(complex) + assert nanoeigenpy.is_approx(A_complex, U @ T @ U.conj().T) + assert nanoeigenpy.is_approx(U @ U.conj().T, np.eye(dim)) -A_triangular = np.triu(A) -cs_triangular = nanoeigenpy.ComplexSchur(dim) -cs_triangular.setMaxIterations(1) -result_triangular = cs_triangular.compute(A_triangular) -assert result_triangular.info() == nanoeigenpy.ComputationInfo.Success + for row in range(1, dim): + for col in range(row): + assert abs(T[row, col]) < 1e-12 -T_triangular = cs_triangular.matrixT() -U_triangular = cs_triangular.matrixU() + A_triangular = np.triu(A) + cs_triangular = nanoeigenpy.ComplexSchur(dim) + cs_triangular.setMaxIterations(1) + result_triangular = cs_triangular.compute(A_triangular) + assert result_triangular.info() == nanoeigenpy.ComputationInfo.Success -A_triangular_complex = A_triangular.astype(complex) -assert nanoeigenpy.is_approx(T_triangular, A_triangular_complex) -assert nanoeigenpy.is_approx(U_triangular, np.eye(dim, dtype=complex)) + T_triangular = cs_triangular.matrixT() + U_triangular = cs_triangular.matrixU() -hess = nanoeigenpy.HessenbergDecomposition(A) -H = hess.matrixH() -Q_hess = hess.matrixQ() + A_triangular_complex = A_triangular.astype(complex) + assert nanoeigenpy.is_approx(T_triangular, A_triangular_complex) + assert nanoeigenpy.is_approx(U_triangular, np.eye(dim, dtype=complex)) -cs_from_hess = nanoeigenpy.ComplexSchur(dim) -result_from_hess = cs_from_hess.computeFromHessenberg(H, Q_hess, True) -assert result_from_hess.info() == nanoeigenpy.ComputationInfo.Success + hess = nanoeigenpy.HessenbergDecomposition(A) + H = hess.matrixH() + Q_hess = hess.matrixQ() -T_from_hess = cs_from_hess.matrixT() -U_from_hess = cs_from_hess.matrixU() + cs_from_hess = nanoeigenpy.ComplexSchur(dim) + result_from_hess = cs_from_hess.computeFromHessenberg(H, Q_hess, True) + assert result_from_hess.info() == nanoeigenpy.ComputationInfo.Success -A_complex = A.astype(complex) -assert nanoeigenpy.is_approx( - A_complex, U_from_hess @ T_from_hess @ U_from_hess.conj().T -) + T_from_hess = cs_from_hess.matrixT() + U_from_hess = cs_from_hess.matrixU() + + A_complex = A.astype(complex) + assert nanoeigenpy.is_approx( + A_complex, U_from_hess @ T_from_hess @ U_from_hess.conj().T + ) diff --git a/tests/test_eigen_solver.py b/tests/test_eigen_solver.py index 27a9594..38f8233 100644 --- a/tests/test_eigen_solver.py +++ b/tests/test_eigen_solver.py @@ -1,40 +1,42 @@ import nanoeigenpy import numpy as np -dim = 100 -rng = np.random.default_rng() -A = rng.random((dim, dim)) +def test_eigen_solver(): + dim = 100 + rng = np.random.default_rng() -es = nanoeigenpy.EigenSolver() -es = nanoeigenpy.EigenSolver(dim) -es = nanoeigenpy.EigenSolver(A) -assert es.info() == nanoeigenpy.ComputationInfo.Success + A = rng.random((dim, dim)) -V = es.eigenvectors() -D = es.eigenvalues() + es = nanoeigenpy.EigenSolver() + es = nanoeigenpy.EigenSolver(dim) + es = nanoeigenpy.EigenSolver(A) + assert es.info() == nanoeigenpy.ComputationInfo.Success -assert nanoeigenpy.is_approx(A.dot(V).real, V.dot(np.diag(D)).real) -assert nanoeigenpy.is_approx(A.dot(V).imag, V.dot(np.diag(D)).imag) + V = es.eigenvectors() + D = es.eigenvalues() -es1 = nanoeigenpy.EigenSolver() -es2 = nanoeigenpy.EigenSolver() + assert nanoeigenpy.is_approx(A.dot(V).real, V.dot(np.diag(D)).real) + assert nanoeigenpy.is_approx(A.dot(V).imag, V.dot(np.diag(D)).imag) -id1 = es1.id() -id2 = es2.id() + es1 = nanoeigenpy.EigenSolver() + es2 = nanoeigenpy.EigenSolver() -assert id1 != id2 -assert id1 == es1.id() -assert id2 == es2.id() + id1 = es1.id() + id2 = es2.id() -dim_constructor = 3 + assert id1 != id2 + assert id1 == es1.id() + assert id2 == es2.id() -es3 = nanoeigenpy.EigenSolver(dim_constructor) -es4 = nanoeigenpy.EigenSolver(dim_constructor) + dim_constructor = 3 -id3 = es3.id() -id4 = es4.id() + es3 = nanoeigenpy.EigenSolver(dim_constructor) + es4 = nanoeigenpy.EigenSolver(dim_constructor) -assert id3 != id4 -assert id3 == es3.id() -assert id4 == es4.id() + id3 = es3.id() + id4 = es4.id() + + assert id3 != id4 + assert id3 == es3.id() + assert id4 == es4.id() diff --git a/tests/test_full_piv_lu.py b/tests/test_full_piv_lu.py index 94e5dcc..25b1c0e 100644 --- a/tests/test_full_piv_lu.py +++ b/tests/test_full_piv_lu.py @@ -1,109 +1,111 @@ import nanoeigenpy import numpy as np -dim = 100 -rng = np.random.default_rng() -A = rng.random((dim, dim)) -A = (A + A.T) * 0.5 + np.diag(10.0 + rng.random(dim)) -fullpivlu = nanoeigenpy.FullPivLU(A) - -X = rng.random((dim, 20)) -B = A.dot(X) -X_est = fullpivlu.solve(B) -assert nanoeigenpy.is_approx(X, X_est) -assert nanoeigenpy.is_approx(A.dot(X_est), B) - -x = rng.random(dim) -b = A.dot(x) -x_est = fullpivlu.solve(b) -assert nanoeigenpy.is_approx(x, x_est) -assert nanoeigenpy.is_approx(A.dot(x_est), b) - -rows = fullpivlu.rows() -cols = fullpivlu.cols() -assert cols == dim -assert rows == dim - -fullpivlu_compute = fullpivlu.compute(A) -A_reconstructed = fullpivlu.reconstructedMatrix() -assert nanoeigenpy.is_approx(A_reconstructed, A) - -nonzeropivots = fullpivlu.nonzeroPivots() -maxpivot = fullpivlu.maxPivot() -assert nonzeropivots == dim -assert maxpivot > 0 - -LU = fullpivlu.matrixLU() -P_perm = fullpivlu.permutationP() -Q_perm = fullpivlu.permutationQ() -P = P_perm.toDenseMatrix() -Q = Q_perm.toDenseMatrix() - -U = np.triu(LU) -L = np.eye(dim) + np.tril(LU, -1) -assert nanoeigenpy.is_approx(P @ A @ Q, L @ U) - -rank = fullpivlu.rank() -dimkernel = fullpivlu.dimensionOfKernel() -injective = fullpivlu.isInjective() -surjective = fullpivlu.isSurjective() -invertible = fullpivlu.isInvertible() -assert rank == dim -assert dimkernel == 0 -assert injective -assert surjective -assert invertible - -kernel = fullpivlu.kernel() -image = fullpivlu.image(A) -assert kernel.shape[1] == 1 -assert nanoeigenpy.is_approx(A @ kernel, np.zeros((dim, 1))) -assert image.shape[1] == rank - -inverse = fullpivlu.inverse() -assert nanoeigenpy.is_approx(A @ inverse, np.eye(dim)) -assert nanoeigenpy.is_approx(inverse @ A, np.eye(dim)) - -rcond = fullpivlu.rcond() -determinant = fullpivlu.determinant() -det_numpy = np.linalg.det(A) -assert rcond > 0 -assert abs(determinant - det_numpy) / abs(det_numpy) < 1e-10 - -fullpivlu.setThreshold() -default_threshold = fullpivlu.threshold() -fullpivlu.setThreshold(1e-8) -assert fullpivlu.threshold() == 1e-8 - -P_inv = P_perm.inverse().toDenseMatrix() -Q_inv = Q_perm.inverse().toDenseMatrix() -assert nanoeigenpy.is_approx(P @ P_inv, np.eye(dim)) -assert nanoeigenpy.is_approx(Q @ Q_inv, np.eye(dim)) -assert nanoeigenpy.is_approx(P_inv @ P, np.eye(dim)) -assert nanoeigenpy.is_approx(Q_inv @ Q, np.eye(dim)) - -rows_rect = 4 -cols_rect = 6 -A_rect = rng.random((rows_rect, cols_rect)) -fullpivlu_rect = nanoeigenpy.FullPivLU(A_rect) -assert fullpivlu_rect.rows() == rows_rect -assert fullpivlu_rect.cols() == cols_rect -rank_rect = fullpivlu_rect.rank() -assert rank_rect <= min(rows_rect, cols_rect) -assert fullpivlu_rect.dimensionOfKernel() == cols_rect - rank_rect - -decomp1 = nanoeigenpy.FullPivLU() -decomp2 = nanoeigenpy.FullPivLU() -id1 = decomp1.id() -id2 = decomp2.id() -assert id1 != id2 -assert id1 == decomp1.id() -assert id2 == decomp2.id() - -decomp3 = nanoeigenpy.FullPivLU(dim, dim) -decomp4 = nanoeigenpy.FullPivLU(dim, dim) -id3 = decomp3.id() -id4 = decomp4.id() -assert id3 != id4 -assert id3 == decomp3.id() -assert id4 == decomp4.id() + +def test_full_piv_lu(): + dim = 100 + rng = np.random.default_rng() + A = rng.random((dim, dim)) + A = (A + A.T) * 0.5 + np.diag(10.0 + rng.random(dim)) + fullpivlu = nanoeigenpy.FullPivLU(A) + + X = rng.random((dim, 20)) + B = A.dot(X) + X_est = fullpivlu.solve(B) + assert nanoeigenpy.is_approx(X, X_est) + assert nanoeigenpy.is_approx(A.dot(X_est), B) + + x = rng.random(dim) + b = A.dot(x) + x_est = fullpivlu.solve(b) + assert nanoeigenpy.is_approx(x, x_est) + assert nanoeigenpy.is_approx(A.dot(x_est), b) + + rows = fullpivlu.rows() + cols = fullpivlu.cols() + assert cols == dim + assert rows == dim + + fullpivlu_compute = fullpivlu.compute(A) # noqa + A_reconstructed = fullpivlu.reconstructedMatrix() + assert nanoeigenpy.is_approx(A_reconstructed, A) + + nonzeropivots = fullpivlu.nonzeroPivots() + maxpivot = fullpivlu.maxPivot() + assert nonzeropivots == dim + assert maxpivot > 0 + + LU = fullpivlu.matrixLU() + P_perm = fullpivlu.permutationP() + Q_perm = fullpivlu.permutationQ() + P = P_perm.toDenseMatrix() + Q = Q_perm.toDenseMatrix() + + U = np.triu(LU) + L = np.eye(dim) + np.tril(LU, -1) + assert nanoeigenpy.is_approx(P @ A @ Q, L @ U) + + rank = fullpivlu.rank() + dimkernel = fullpivlu.dimensionOfKernel() + injective = fullpivlu.isInjective() + surjective = fullpivlu.isSurjective() + invertible = fullpivlu.isInvertible() + assert rank == dim + assert dimkernel == 0 + assert injective + assert surjective + assert invertible + + kernel = fullpivlu.kernel() + image = fullpivlu.image(A) + assert kernel.shape[1] == 1 + assert nanoeigenpy.is_approx(A @ kernel, np.zeros((dim, 1))) + assert image.shape[1] == rank + + inverse = fullpivlu.inverse() + assert nanoeigenpy.is_approx(A @ inverse, np.eye(dim)) + assert nanoeigenpy.is_approx(inverse @ A, np.eye(dim)) + + rcond = fullpivlu.rcond() + determinant = fullpivlu.determinant() + det_numpy = np.linalg.det(A) + assert rcond > 0 + assert abs(determinant - det_numpy) / abs(det_numpy) < 1e-10 + + fullpivlu.setThreshold() + default_threshold = fullpivlu.threshold() # noqa + fullpivlu.setThreshold(1e-8) + assert fullpivlu.threshold() == 1e-8 + + P_inv = P_perm.inverse().toDenseMatrix() + Q_inv = Q_perm.inverse().toDenseMatrix() + assert nanoeigenpy.is_approx(P @ P_inv, np.eye(dim)) + assert nanoeigenpy.is_approx(Q @ Q_inv, np.eye(dim)) + assert nanoeigenpy.is_approx(P_inv @ P, np.eye(dim)) + assert nanoeigenpy.is_approx(Q_inv @ Q, np.eye(dim)) + + rows_rect = 4 + cols_rect = 6 + A_rect = rng.random((rows_rect, cols_rect)) + fullpivlu_rect = nanoeigenpy.FullPivLU(A_rect) + assert fullpivlu_rect.rows() == rows_rect + assert fullpivlu_rect.cols() == cols_rect + rank_rect = fullpivlu_rect.rank() + assert rank_rect <= min(rows_rect, cols_rect) + assert fullpivlu_rect.dimensionOfKernel() == cols_rect - rank_rect + + decomp1 = nanoeigenpy.FullPivLU() + decomp2 = nanoeigenpy.FullPivLU() + id1 = decomp1.id() + id2 = decomp2.id() + assert id1 != id2 + assert id1 == decomp1.id() + assert id2 == decomp2.id() + + decomp3 = nanoeigenpy.FullPivLU(dim, dim) + decomp4 = nanoeigenpy.FullPivLU(dim, dim) + id3 = decomp3.id() + id4 = decomp4.id() + assert id3 != id4 + assert id3 == decomp3.id() + assert id4 == decomp4.id() diff --git a/tests/test_generalized_eigen_solver.py b/tests/test_generalized_eigen_solver.py index 4be542c..2bf2c01 100644 --- a/tests/test_generalized_eigen_solver.py +++ b/tests/test_generalized_eigen_solver.py @@ -1,40 +1,42 @@ import nanoeigenpy import numpy as np -dim = 100 -rng = np.random.default_rng() -A = rng.random((dim, dim)) -B = rng.random((dim, dim)) -B = (B + B.T) * 0.5 + np.diag(10.0 + rng.random(dim)) - -ges_matrices = nanoeigenpy.GeneralizedEigenSolver(A, B) -assert ges_matrices.info() == nanoeigenpy.ComputationInfo.Success - -alphas = ges_matrices.alphas() -betas = ges_matrices.betas() -eigenvectors = ges_matrices.eigenvectors() -eigenvalues = ges_matrices.eigenvalues() - -for k in range(dim): - v = eigenvectors[:, k] - lambda_k = eigenvalues[k] - - Av = A @ v - lambda_Bv = lambda_k * (B @ v) - assert nanoeigenpy.is_approx(Av.real, lambda_Bv.real, 1e-6) - assert nanoeigenpy.is_approx(Av.imag, lambda_Bv.imag, 1e-6) - -for k in range(dim): - v = eigenvectors[:, k] - alpha = alphas[k] - beta = betas[k] - - alpha_Bv = alpha * (B @ v) - beta_Av = beta * (A @ v) - assert nanoeigenpy.is_approx(alpha_Bv.real, beta_Av.real, 1e-6) - assert nanoeigenpy.is_approx(alpha_Bv.imag, beta_Av.imag, 1e-6) - -for k in range(dim): - if abs(betas[k]) > 1e-12: - expected_eigenvalue = alphas[k] / betas[k] - assert abs(eigenvalues[k] - expected_eigenvalue) < 1e-12 + +def test_generalized_eigen_solver(): + dim = 100 + rng = np.random.default_rng() + A = rng.random((dim, dim)) + B = rng.random((dim, dim)) + B = (B + B.T) * 0.5 + np.diag(10.0 + rng.random(dim)) + + ges_matrices = nanoeigenpy.GeneralizedEigenSolver(A, B) + assert ges_matrices.info() == nanoeigenpy.ComputationInfo.Success + + alphas = ges_matrices.alphas() + betas = ges_matrices.betas() + eigenvectors = ges_matrices.eigenvectors() + eigenvalues = ges_matrices.eigenvalues() + + for k in range(dim): + v = eigenvectors[:, k] + lambda_k = eigenvalues[k] + + Av = A @ v + lambda_Bv = lambda_k * (B @ v) + assert nanoeigenpy.is_approx(Av.real, lambda_Bv.real, 1e-6) + assert nanoeigenpy.is_approx(Av.imag, lambda_Bv.imag, 1e-6) + + for k in range(dim): + v = eigenvectors[:, k] + alpha = alphas[k] + beta = betas[k] + + alpha_Bv = alpha * (B @ v) + beta_Av = beta * (A @ v) + assert nanoeigenpy.is_approx(alpha_Bv.real, beta_Av.real, 1e-6) + assert nanoeigenpy.is_approx(alpha_Bv.imag, beta_Av.imag, 1e-6) + + for k in range(dim): + if abs(betas[k]) > 1e-12: + expected_eigenvalue = alphas[k] / betas[k] + assert abs(eigenvalues[k] - expected_eigenvalue) < 1e-12 diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 1fe48ab..7097178 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -1,7 +1,7 @@ -import numpy as np -from numpy import cos import nanoeigenpy import quaternion +import numpy as np +from numpy import cos verbose = True @@ -14,692 +14,699 @@ def isapprox(a, b, epsilon=1e-6): return abs(a - b) < epsilon -# --- Quaternion --------------------------------------------------------------- -verbose and print("[Quaternion] Coefficient initialisation") -q = nanoeigenpy.Quaternion(1, 2, 3, 4) -q.normalize() -assert isapprox(np.linalg.norm(q.coeffs()), q.norm()) -assert isapprox(np.linalg.norm(q.coeffs()), 1) - -verbose and print("[Quaternion] Coefficient-vector initialisation") -v = np.array([0.5, -0.5, 0.5, 0.5]) -for k in range(10000): - qv = nanoeigenpy.Quaternion(v) -assert isapprox(qv.coeffs(), v) - -verbose and print("[Quaternion] AngleAxis initialisation") -r = nanoeigenpy.AngleAxis(q) -q2 = nanoeigenpy.Quaternion(r) -assert q == q -assert isapprox(q.coeffs(), q2.coeffs()) -assert q2.isApprox(q2) -assert q2.isApprox(q2, 1e-2) - -Rq = q.matrix() -Rr = r.matrix() -assert isapprox(Rq.dot(Rq.T), np.eye(3)) -assert isapprox(Rr, Rq) - -verbose and print("[Quaternion] Rotation Matrix initialisation") -qR = nanoeigenpy.Quaternion(Rr) -assert q.isApprox(qR) -assert isapprox(q.coeffs(), qR.coeffs()) - -assert isapprox(qR[3], 1.0 / np.sqrt(30)) -try: - qR[5] - print("Error, this message should not appear.") -except IndexError as e: +def test_geometry(): + # --- Quaternion --------------------------------------------------------------- + verbose and print("[Quaternion] Coefficient initialisation") + q = nanoeigenpy.Quaternion(1, 2, 3, 4) + q.normalize() + assert isapprox(np.linalg.norm(q.coeffs()), q.norm()) + assert isapprox(np.linalg.norm(q.coeffs()), 1) + + verbose and print("[Quaternion] Coefficient-vector initialisation") + v = np.array([0.5, -0.5, 0.5, 0.5]) + for k in range(10000): + qv = nanoeigenpy.Quaternion(v) + assert isapprox(qv.coeffs(), v) + + verbose and print("[Quaternion] AngleAxis initialisation") + r = nanoeigenpy.AngleAxis(q) + q2 = nanoeigenpy.Quaternion(r) + assert q == q + assert isapprox(q.coeffs(), q2.coeffs()) + assert q2.isApprox(q2) + assert q2.isApprox(q2, 1e-2) + + Rq = q.matrix() + Rr = r.matrix() + assert isapprox(Rq.dot(Rq.T), np.eye(3)) + assert isapprox(Rr, Rq) + + verbose and print("[Quaternion] Rotation Matrix initialisation") + qR = nanoeigenpy.Quaternion(Rr) + assert q.isApprox(qR) + assert isapprox(q.coeffs(), qR.coeffs()) + + assert isapprox(qR[3], 1.0 / np.sqrt(30)) + try: + qR[5] + print("Error, this message should not appear.") + except IndexError as e: + if verbose: + print("As expected, caught exception: ", e) + + x = quaternion.X(q) + assert x.a == q + + # --- Angle Vector ------------------------------------------------ + r = nanoeigenpy.AngleAxis(0.1, np.array([1, 0, 0], np.double)) if verbose: - print("As expected, caught exception: ", e) - -x = quaternion.X(q) -assert x.a == q - -# --- Angle Vector ------------------------------------------------ -r = nanoeigenpy.AngleAxis(0.1, np.array([1, 0, 0], np.double)) -if verbose: - print("Rx(.1) = \n\n", r.matrix(), "\n") -assert isapprox(r.matrix()[2, 2], cos(r.angle)) -assert isapprox(r.axis, np.array([1.0, 0, 0])) -assert isapprox(r.angle, 0.1) -assert r.isApprox(r) -assert r.isApprox(r, 1e-2) - -r.axis = np.array([0, 1, 0], np.double).T -assert isapprox(r.matrix()[0, 0], cos(r.angle)) - -ri = r.inverse() -assert isapprox(ri.angle, -0.1) - -R = r.matrix() -r2 = nanoeigenpy.AngleAxis(np.dot(R, R)) -assert isapprox(r2.angle, r.angle * 2) - -# --- Hyperplane ------------------------------------------------ -verbose and print("[Hyperplane] Normal and point construction") -n = np.array([1.0, 0.0]) -p = np.array([2.0, 3.0]) -h = nanoeigenpy.Hyperplane(n, p) -assert isapprox(h.normal(), n) -assert isapprox(h.absDistance(p), 0.0) -assert h.dim() == 2 - -verbose and print("[Hyperplane] Normal and distance construction") -d = -np.dot(n, p) -h2 = nanoeigenpy.Hyperplane(n, d) -assert isapprox(h.coeffs(), h2.coeffs()) -assert isapprox(h2.offset(), d) - -verbose and print("[Hyperplane] Through two points") -p1 = np.array([0.0, 0.0]) -p2 = np.array([1.0, 1.0]) -h3 = nanoeigenpy.Hyperplane.Through(p1, p2) -assert isapprox(h3.absDistance(p1), 0.0) -assert isapprox(h3.absDistance(p2), 0.0) -assert isapprox(np.linalg.norm(h3.normal()), 1.0) - -verbose and print("[Hyperplane] Through three points") -p1_3d = np.array([1.0, 0.0, 0.0]) -p2_3d = np.array([0.0, 1.0, 0.0]) -p3_3d = np.array([0.0, 0.0, 1.0]) -h4 = nanoeigenpy.Hyperplane.Through(p1_3d, p2_3d, p3_3d) -assert isapprox(h4.absDistance(p1_3d), 0.0) -assert isapprox(h4.absDistance(p2_3d), 0.0) -assert isapprox(h4.absDistance(p3_3d), 0.0) -assert isapprox(np.linalg.norm(h4.normal()), 1.0) -assert h4.dim() == 3 - -verbose and print("[Hyperplane] Distance calculations") -test_point = np.array([1.0, 0.0]) -signed_dist = h3.signedDistance(test_point) -abs_dist = h3.absDistance(test_point) -assert isapprox(abs_dist, abs(signed_dist)) - -verbose and print("[Hyperplane] Projection") -proj = h3.projection(test_point) -assert isapprox(h3.absDistance(proj), 0.0) - -verbose and print("[Hyperplane] Normalization") -h_copy = nanoeigenpy.Hyperplane(np.array([2.0, 0.0]), np.array([1.0, 0.0])) -h_copy.normalize() -assert isapprox(np.linalg.norm(h_copy.normal()), 1.0) - -verbose and print("[Hyperplane] Line intersection") -h_line1 = nanoeigenpy.Hyperplane(np.array([1.0, 0.0]), 0.0) -h_line2 = nanoeigenpy.Hyperplane(np.array([0.0, 1.0]), 0.0) -intersection = h_line1.intersection(h_line2) -assert isapprox(intersection, np.array([0.0, 0.0])) - -verbose and print("[Hyperplane] isApprox") -h5 = nanoeigenpy.Hyperplane(h) -assert h.isApprox(h5) -assert h.isApprox(h5, 1e-12) - -# --- ParametrizedLine ------------------------------------------------ -verbose and print("[ParametrizedLine] Origin and direction construction") -origin = np.array([1.0, 2.0]) -direction = np.array([1.0, 0.0]) -line = nanoeigenpy.ParametrizedLine(origin, direction) -assert isapprox(line.origin(), origin) -assert isapprox(line.direction(), direction) -assert line.dim() == 2 - -verbose and print("[ParametrizedLine] Default constructor") -line_default = nanoeigenpy.ParametrizedLine() -assert line_default.dim() == 0 - -verbose and print("[ParametrizedLine] Dimension constructor") -line_3d = nanoeigenpy.ParametrizedLine(3) -assert line_3d.dim() == 3 - -verbose and print("[ParametrizedLine] Copy constructor") -line_copy = nanoeigenpy.ParametrizedLine(line) -assert isapprox(line_copy.origin(), line.origin()) -assert isapprox(line_copy.direction(), line.direction()) - -verbose and print("[ParametrizedLine] Construction from 2D hyperplane") -h_2d = nanoeigenpy.Hyperplane(np.array([1.0, 0.0]), 0.0) -line_from_h = nanoeigenpy.ParametrizedLine(h_2d) -assert line_from_h.dim() == 2 -assert isapprox(line_from_h.origin(), np.array([0.0, 0.0])) -assert isapprox(line_from_h.direction(), np.array([0.0, 1.0])) - -verbose and print("[ParametrizedLine] 3D hyperplane should fail") -h_3d = nanoeigenpy.Hyperplane(np.array([1.0, 0.0, 0.0]), 0.0) -try: - line_fail = nanoeigenpy.ParametrizedLine(h_3d) - print("Error, this message should not appear.") -except ValueError as e: - if verbose: - print("As expected, caught exception:", e) - -verbose and print("[ParametrizedLine] Distance calculations") -test_point = np.array([1.0, 0.0]) -line_x_axis = nanoeigenpy.ParametrizedLine(np.array([0.0, 0.0]), np.array([1.0, 0.0])) -distance = line_x_axis.distance(test_point) -squared_distance = line_x_axis.squaredDistance(test_point) -assert isapprox(distance, 0.0) -assert isapprox(squared_distance, 0.0) - -off_line_point = np.array([1.0, 1.0]) -distance_off = line_x_axis.distance(off_line_point) -squared_distance_off = line_x_axis.squaredDistance(off_line_point) -assert isapprox(distance_off, 1.0) -assert isapprox(squared_distance_off, 1.0) -assert isapprox(distance_off * distance_off, squared_distance_off) - -verbose and print("[ParametrizedLine] Projection") -projection = line_x_axis.projection(off_line_point) -assert isapprox(projection, np.array([1.0, 0.0])) -assert isapprox(line_x_axis.distance(projection), 0.0) - -verbose and print("[ParametrizedLine] Intersection with hyperplane") -line_diagonal = nanoeigenpy.ParametrizedLine(np.array([0.0, 0.0]), np.array([1.0, 1.0])) -h_vertical = nanoeigenpy.Hyperplane(np.array([1.0, 0.0]), -1.0) - -intersection_param = line_diagonal.intersectionParameter(h_vertical) -assert isapprox(intersection_param, 1.0) - -intersection_param_old = line_diagonal.intersection(h_vertical) -assert isapprox(intersection_param_old, intersection_param) - -intersection_point = line_diagonal.intersectionPoint(h_vertical) -expected_intersection = np.array([1.0, 1.0]) -assert isapprox(intersection_point, expected_intersection) -assert isapprox(h_vertical.absDistance(intersection_point), 0.0) - -verbose and print("[ParametrizedLine] isApprox") -line1 = nanoeigenpy.ParametrizedLine(np.array([0.0, 0.0]), np.array([1.0, 0.0])) -line2 = nanoeigenpy.ParametrizedLine(np.array([0.0, 0.0]), np.array([1.0, 0.0])) -line3 = nanoeigenpy.ParametrizedLine(np.array([0.0, 0.0]), np.array([0.0, 1.0])) -assert line1.isApprox(line2) -assert line1.isApprox(line2, 1e-12) -assert not line1.isApprox(line3) - -verbose and print("[ParametrizedLine] Parallel lines") -line_parallel1 = nanoeigenpy.ParametrizedLine( - np.array([0.0, 0.0]), np.array([1.0, 0.0]) -) -line_parallel2 = nanoeigenpy.ParametrizedLine( - np.array([0.0, 1.0]), np.array([1.0, 0.0]) -) -assert not line_parallel1.isApprox(line_parallel2) - -test_points = [np.array([i, 0.0]) for i in range(5)] -distances = [line_parallel2.distance(p) for p in test_points] -for d in distances: - assert isapprox(d, 1.0) - -verbose and print("[ParametrizedLine] Through two points") -p0 = np.array([0.0, 0.0]) -p1 = np.array([1.0, 1.0]) -line_through = nanoeigenpy.ParametrizedLine.Through(p0, p1) -direction = line_through.direction() -expected_dir = (p1 - p0) / np.linalg.norm(p1 - p0) -assert isapprox(line_through.origin(), p0) -assert isapprox(np.linalg.norm(direction), 1.0) -assert isapprox(direction, expected_dir) - -# --- Rotation2D ------------------------------------------------ -verbose and print("[Rotation2D] Default constructor") -r_default = nanoeigenpy.Rotation2D() -assert isapprox(r_default.angle, 0.0) - -verbose and print("[Rotation2D] Angle constructor") -angle = np.pi / 4 -r_angle = nanoeigenpy.Rotation2D(angle) -assert isapprox(r_angle.angle, angle) - -verbose and print("[Rotation2D] Copy constructor") -r_copy = nanoeigenpy.Rotation2D(r_angle) -assert isapprox(r_copy.angle, r_angle.angle) -assert r_copy == r_angle - -verbose and print("[Rotation2D] Matrix constructor") -theta = np.pi / 6 -rotation_matrix = np.array( - [[np.cos(theta), -np.sin(theta)], [np.sin(theta), np.cos(theta)]] -) -r_matrix = nanoeigenpy.Rotation2D(rotation_matrix) -assert isapprox(r_matrix.angle, theta) - -verbose and print("[Rotation2D] Angle property") -r_prop = nanoeigenpy.Rotation2D() -new_angle = np.pi / 3 -r_prop.angle = new_angle -assert isapprox(r_prop.angle, new_angle) - -verbose and print("[Rotation2D] smallestPositiveAngle") -r_negative = nanoeigenpy.Rotation2D(-np.pi / 4) -positive_angle = r_negative.smallestPositiveAngle() -assert positive_angle >= 0.0 -assert positive_angle < 2 * np.pi -assert isapprox(positive_angle, 7 * np.pi / 4) - -verbose and print("[Rotation2D] smallestAngle") -r_large = nanoeigenpy.Rotation2D(3 * np.pi) -smallest_angle = r_large.smallestAngle() -assert smallest_angle >= -np.pi -assert smallest_angle <= np.pi -assert isapprox(smallest_angle, np.pi) - -verbose and print("[Rotation2D] Identity") -r_identity = nanoeigenpy.Rotation2D.Identity() -assert isapprox(r_identity.angle, 0.0) - -verbose and print("[Rotation2D] fromRotationMatrix") -r_from_matrix = nanoeigenpy.Rotation2D() -theta2 = np.pi / 2 -matrix2 = np.array( - [[np.cos(theta2), -np.sin(theta2)], [np.sin(theta2), np.cos(theta2)]] -) -r_from_matrix.fromRotationMatrix(matrix2) -assert isapprox(r_from_matrix.angle, theta2) - -verbose and print("[Rotation2D] Rotation composition") -r1 = nanoeigenpy.Rotation2D(np.pi / 4) -r2 = nanoeigenpy.Rotation2D(np.pi / 6) -r_composed = r1 * r2 -expected_angle = np.pi / 4 + np.pi / 6 -assert isapprox(r_composed.angle, expected_angle) - -verbose and print("[Rotation2D] In-place multiplication") -r_inplace = nanoeigenpy.Rotation2D(np.pi / 4) -original_angle = r_inplace.angle -r_inplace *= nanoeigenpy.Rotation2D(np.pi / 6) -assert isapprox(r_inplace.angle, original_angle + np.pi / 6) - -verbose and print("[Rotation2D] Vector rotation") -r_90 = nanoeigenpy.Rotation2D(np.pi / 2) -vec = np.array([1.0, 0.0]) -rotated_vec = r_90 * vec -expected_vec = np.array([0.0, 1.0]) -assert isapprox(rotated_vec, expected_vec) - -vec2 = np.array([1.0, 1.0]) -r_45 = nanoeigenpy.Rotation2D(np.pi / 4) -rotated_vec2 = r_45 * vec2 -expected_vec2 = np.array([0.0, np.sqrt(2)]) -assert isapprox(rotated_vec2, expected_vec2) - -verbose and print("[Rotation2D] Equality operators") -r_eq1 = nanoeigenpy.Rotation2D(np.pi / 3) -r_eq2 = nanoeigenpy.Rotation2D(np.pi / 3) -r_eq3 = nanoeigenpy.Rotation2D(np.pi / 4) - -assert r_eq1 == r_eq2 -assert not (r_eq1 == r_eq3) -assert r_eq1 != r_eq3 -assert not (r_eq1 != r_eq2) - -verbose and print("[Rotation2D] Periodic angles") -r_period1 = nanoeigenpy.Rotation2D(0.0) -r_period2 = nanoeigenpy.Rotation2D(2 * np.pi) -verbose and print("[Rotation2D] isApprox") -r_approx1 = nanoeigenpy.Rotation2D(np.pi / 4) -r_approx2 = nanoeigenpy.Rotation2D(np.pi / 4 + 1e-15) -r_approx3 = nanoeigenpy.Rotation2D(np.pi / 3) - -assert r_approx1.isApprox(r_approx2) -assert r_approx1.isApprox(r_approx2, 1e-12) -assert not r_approx1.isApprox(r_approx3) - -verbose and print("[Rotation2D] slerp") -r_start = nanoeigenpy.Rotation2D(0.0) -r_end = nanoeigenpy.Rotation2D(np.pi / 2) -r_middle = r_start.slerp(0.5, r_end) -assert isapprox(r_middle.angle, np.pi / 4) - -r_slerp_0 = r_start.slerp(0.0, r_end) -r_slerp_1 = r_start.slerp(1.0, r_end) -assert isapprox(r_slerp_0.angle, r_start.angle) -assert isapprox(r_slerp_1.angle, r_end.angle) - -verbose and print("[Rotation2D] Inverse rotation") -try: - r_original = nanoeigenpy.Rotation2D(np.pi / 3) - r_inverse = r_original.inverse() - assert isapprox(r_inverse.angle, -np.pi / 3) - - r_identity_test = r_original * r_inverse - assert isapprox(r_identity_test.angle, 0.0, 1e-12) -except AttributeError: - if verbose: - print("inverse() method not exposed or not available") - -verbose and print("[Rotation2D] Matrix conversion") -try: - r_matrix_test = nanoeigenpy.Rotation2D(np.pi / 6) - matrix = r_matrix_test.matrix() - - assert matrix.shape == (2, 2) - assert isapprox(matrix @ matrix.T, np.eye(2)) - assert isapprox(np.linalg.det(matrix), 1.0) - - expected_matrix = np.array( - [ - [np.cos(np.pi / 6), -np.sin(np.pi / 6)], - [np.sin(np.pi / 6), np.cos(np.pi / 6)], - ] + print("Rx(.1) = \n\n", r.matrix(), "\n") + assert isapprox(r.matrix()[2, 2], cos(r.angle)) + assert isapprox(r.axis, np.array([1.0, 0, 0])) + assert isapprox(r.angle, 0.1) + assert r.isApprox(r) + assert r.isApprox(r, 1e-2) + + r.axis = np.array([0, 1, 0], np.double).T + assert isapprox(r.matrix()[0, 0], cos(r.angle)) + + ri = r.inverse() + assert isapprox(ri.angle, -0.1) + + R = r.matrix() + r2 = nanoeigenpy.AngleAxis(np.dot(R, R)) + assert isapprox(r2.angle, r.angle * 2) + + # --- Hyperplane ------------------------------------------------ + verbose and print("[Hyperplane] Normal and point construction") + n = np.array([1.0, 0.0]) + p = np.array([2.0, 3.0]) + h = nanoeigenpy.Hyperplane(n, p) + assert isapprox(h.normal(), n) + assert isapprox(h.absDistance(p), 0.0) + assert h.dim() == 2 + + verbose and print("[Hyperplane] Normal and distance construction") + d = -np.dot(n, p) + h2 = nanoeigenpy.Hyperplane(n, d) + assert isapprox(h.coeffs(), h2.coeffs()) + assert isapprox(h2.offset(), d) + + verbose and print("[Hyperplane] Through two points") + p1 = np.array([0.0, 0.0]) + p2 = np.array([1.0, 1.0]) + h3 = nanoeigenpy.Hyperplane.Through(p1, p2) + assert isapprox(h3.absDistance(p1), 0.0) + assert isapprox(h3.absDistance(p2), 0.0) + assert isapprox(np.linalg.norm(h3.normal()), 1.0) + + verbose and print("[Hyperplane] Through three points") + p1_3d = np.array([1.0, 0.0, 0.0]) + p2_3d = np.array([0.0, 1.0, 0.0]) + p3_3d = np.array([0.0, 0.0, 1.0]) + h4 = nanoeigenpy.Hyperplane.Through(p1_3d, p2_3d, p3_3d) + assert isapprox(h4.absDistance(p1_3d), 0.0) + assert isapprox(h4.absDistance(p2_3d), 0.0) + assert isapprox(h4.absDistance(p3_3d), 0.0) + assert isapprox(np.linalg.norm(h4.normal()), 1.0) + assert h4.dim() == 3 + + verbose and print("[Hyperplane] Distance calculations") + test_point = np.array([1.0, 0.0]) + signed_dist = h3.signedDistance(test_point) + abs_dist = h3.absDistance(test_point) + assert isapprox(abs_dist, abs(signed_dist)) + + verbose and print("[Hyperplane] Projection") + proj = h3.projection(test_point) + assert isapprox(h3.absDistance(proj), 0.0) + + verbose and print("[Hyperplane] Normalization") + h_copy = nanoeigenpy.Hyperplane(np.array([2.0, 0.0]), np.array([1.0, 0.0])) + h_copy.normalize() + assert isapprox(np.linalg.norm(h_copy.normal()), 1.0) + + verbose and print("[Hyperplane] Line intersection") + h_line1 = nanoeigenpy.Hyperplane(np.array([1.0, 0.0]), 0.0) + h_line2 = nanoeigenpy.Hyperplane(np.array([0.0, 1.0]), 0.0) + intersection = h_line1.intersection(h_line2) + assert isapprox(intersection, np.array([0.0, 0.0])) + + verbose and print("[Hyperplane] isApprox") + h5 = nanoeigenpy.Hyperplane(h) + assert h.isApprox(h5) + assert h.isApprox(h5, 1e-12) + + # --- ParametrizedLine ------------------------------------------------ + verbose and print("[ParametrizedLine] Origin and direction construction") + origin = np.array([1.0, 2.0]) + direction = np.array([1.0, 0.0]) + line = nanoeigenpy.ParametrizedLine(origin, direction) + assert isapprox(line.origin(), origin) + assert isapprox(line.direction(), direction) + assert line.dim() == 2 + + verbose and print("[ParametrizedLine] Default constructor") + line_default = nanoeigenpy.ParametrizedLine() + assert line_default.dim() == 0 + + verbose and print("[ParametrizedLine] Dimension constructor") + line_3d = nanoeigenpy.ParametrizedLine(3) + assert line_3d.dim() == 3 + + verbose and print("[ParametrizedLine] Copy constructor") + line_copy = nanoeigenpy.ParametrizedLine(line) + assert isapprox(line_copy.origin(), line.origin()) + assert isapprox(line_copy.direction(), line.direction()) + + verbose and print("[ParametrizedLine] Construction from 2D hyperplane") + h_2d = nanoeigenpy.Hyperplane(np.array([1.0, 0.0]), 0.0) + line_from_h = nanoeigenpy.ParametrizedLine(h_2d) + assert line_from_h.dim() == 2 + assert isapprox(line_from_h.origin(), np.array([0.0, 0.0])) + assert isapprox(line_from_h.direction(), np.array([0.0, 1.0])) + + verbose and print("[ParametrizedLine] 3D hyperplane should fail") + h_3d = nanoeigenpy.Hyperplane(np.array([1.0, 0.0, 0.0]), 0.0) + try: + line_fail = nanoeigenpy.ParametrizedLine(h_3d) # noqa + print("Error, this message should not appear.") + except ValueError as e: + if verbose: + print("As expected, caught exception:", e) + + verbose and print("[ParametrizedLine] Distance calculations") + test_point = np.array([1.0, 0.0]) + line_x_axis = nanoeigenpy.ParametrizedLine( + np.array([0.0, 0.0]), np.array([1.0, 0.0]) ) - assert isapprox(matrix, expected_matrix) - -except AttributeError: - if verbose: - print("matrix() method not exposed or not available") - -verbose and print("[Rotation2D] Angle normalization") -r_large_angle = nanoeigenpy.Rotation2D(3 * np.pi) -vec_test = np.array([1.0, 0.0]) -rotated_large = r_large_angle * vec_test -expected_large = np.array([-1.0, 0.0]) -assert isapprox(rotated_large, expected_large) - -# --- UniformScaling ------------------------------------------------ -verbose and print("[UniformScaling] Default constructor") -s_default = nanoeigenpy.UniformScaling() - -verbose and print("[UniformScaling] Factor constructor") -factor = 2.5 -s_factor = nanoeigenpy.UniformScaling(factor) -assert isapprox(s_factor.factor(), factor) - -verbose and print("[UniformScaling] Copy constructor") -s_copy = nanoeigenpy.UniformScaling(s_factor) -assert isapprox(s_copy.factor(), s_factor.factor()) - -verbose and print("[UniformScaling] Factor getter") -s_test = nanoeigenpy.UniformScaling(3.0) -assert isapprox(s_test.factor(), 3.0) - -verbose and print("[UniformScaling] Inverse scaling") -s_original = nanoeigenpy.UniformScaling(4.0) -s_inverse = s_original.inverse() -assert isapprox(s_inverse.factor(), 1.0 / 4.0) - -s_identity_test = s_original * s_inverse -assert isapprox(s_identity_test.factor(), 1.0) - -verbose and print("[UniformScaling] Concatenation of scalings") -s1 = nanoeigenpy.UniformScaling(2.0) -s2 = nanoeigenpy.UniformScaling(3.0) -s_combined = s1 * s2 -assert isapprox(s_combined.factor(), 6.0) - -verbose and print("[UniformScaling] Multiplication with matrix") -s_scale = nanoeigenpy.UniformScaling(2.0) -matrix = np.array([[1.0, 2.0], [3.0, 4.0]]) -scaled_matrix = s_scale * matrix -expected_matrix = matrix * 2.0 -assert isapprox(scaled_matrix, expected_matrix) - -identity = np.eye(3) -s_identity_scale = nanoeigenpy.UniformScaling(5.0) -scaled_identity = s_identity_scale * identity -expected_identity = identity * 5.0 -assert isapprox(scaled_identity, expected_identity) - -verbose and print("[UniformScaling] Multiplication with AngleAxis") -try: - angle_axis = nanoeigenpy.AngleAxis(np.pi / 4, np.array([0.0, 0.0, 1.0])) - s_with_rotation = nanoeigenpy.UniformScaling(2.0) - result_rotation = s_with_rotation * angle_axis - - assert result_rotation.shape == (3, 3) - det = np.linalg.det(result_rotation) - assert isapprox(det, 2.0**3) - -except (AttributeError, NameError): - if verbose: - print("AngleAxis class not available or not exposed") - -verbose and print("[UniformScaling] Multiplication with Quaternion") -try: - quat = nanoeigenpy.Quaternion(1, 0, 0, 0) - s_with_quat = nanoeigenpy.UniformScaling(3.0) - result_quat = s_with_quat * quat - - assert result_quat.shape == (3, 3) - expected_scaled_identity = np.eye(3) * 3.0 - assert isapprox(result_quat, expected_scaled_identity) - -except (AttributeError, NameError): - if verbose: - print("Quaternion class not available or not exposed") - -verbose and print("[UniformScaling] Multiplication with Rotation2D") -try: - rotation_2d = nanoeigenpy.Rotation2D(np.pi / 4) - s_with_rot2d = nanoeigenpy.UniformScaling(2.0) - result_rot2d = s_with_rot2d * rotation_2d - - assert result_rot2d.shape == (2, 2) - det_2d = np.linalg.det(result_rot2d) - assert isapprox(det_2d, 2.0**2) - -except (AttributeError, NameError): - if verbose: - print("Rotation2D class not available or not exposed") - -verbose and print("[UniformScaling] isApprox") -s_approx1 = nanoeigenpy.UniformScaling(2.0) -s_approx2 = nanoeigenpy.UniformScaling(2.0 + 1e-15) -s_approx3 = nanoeigenpy.UniformScaling(3.0) - -assert s_approx1.isApprox(s_approx2) -assert s_approx1.isApprox(s_approx2, 1e-12) -assert not s_approx1.isApprox(s_approx3) - -verbose and print("[UniformScaling] Edge cases") -s_zero = nanoeigenpy.UniformScaling(0.0) -assert isapprox(s_zero.factor(), 0.0) -try: - s_zero_inverse = s_zero.inverse() - if verbose: - print("Zero scaling inverse:", s_zero_inverse.factor()) -except Exception as e: - if verbose: - print("Zero scaling inverse threw exception (expected):", type(e).__name__) - -s_negative = nanoeigenpy.UniformScaling(-2.0) -assert isapprox(s_negative.factor(), -2.0) -s_negative_inverse = s_negative.inverse() -assert isapprox(s_negative_inverse.factor(), -0.5) - -s_small = nanoeigenpy.UniformScaling(1e-10) -s_small_inverse = s_small.inverse() -assert isapprox(s_small_inverse.factor(), 1e10) - -verbose and print("[UniformScaling] Chain operations") -s_chain1 = nanoeigenpy.UniformScaling(2.0) -s_chain2 = nanoeigenpy.UniformScaling(3.0) -s_chain3 = nanoeigenpy.UniformScaling(4.0) - -left_assoc = (s_chain1 * s_chain2) * s_chain3 -right_assoc = s_chain1 * (s_chain2 * s_chain3) -assert isapprox(left_assoc.factor(), right_assoc.factor()) -assert isapprox(left_assoc.factor(), 24.0) - -verbose and print("[UniformScaling] Vector scaling") -s_vector = nanoeigenpy.UniformScaling(2.0) -vector = np.array([[1.0], [2.0], [3.0]]) -identity_3x3 = np.eye(3) -scaled_identity = s_vector * identity_3x3 -scaled_vector = scaled_identity @ vector -expected_vector = vector * 2.0 -assert isapprox(scaled_vector, expected_vector) - -# --- Translation ------------------------------------------------ -verbose and print("[Translation] Default constructor") -t_default = nanoeigenpy.Translation() - -verbose and print("[Translation] 2D constructor with vector") -t_2d = nanoeigenpy.Translation(np.array([1.0, 2.0])) -assert isapprox(t_2d.x, 1.0) -assert isapprox(t_2d.y, 2.0) - -verbose and print("[Translation] 3D constructor with vector") -t_3d = nanoeigenpy.Translation(np.array([1.0, 2.0, 3.0])) -assert isapprox(t_3d.x, 1.0) -assert isapprox(t_3d.y, 2.0) -assert isapprox(t_3d.z, 3.0) - -verbose and print("[Translation] Vector constructor") -vector = np.array([1.5, 2.5, 3.5]) -t_vector = nanoeigenpy.Translation(vector) -assert isapprox(t_vector.x, 1.5) -assert isapprox(t_vector.y, 2.5) -assert isapprox(t_vector.z, 3.5) - -verbose and print("[Translation] Copy constructor") -t_copy = nanoeigenpy.Translation(t_3d) -assert isapprox(t_copy.x, t_3d.x) -assert isapprox(t_copy.y, t_3d.y) -assert isapprox(t_copy.z, t_3d.z) - -verbose and print("[Translation] Property setters") -t_test = nanoeigenpy.Translation(np.array([1.0, 2.0, 3.0])) -t_test.x = 10.0 -t_test.y = 20.0 -t_test.z = 30.0 -assert isapprox(t_test.x, 10.0) -assert isapprox(t_test.y, 20.0) -assert isapprox(t_test.z, 30.0) - -verbose and print("[Translation] Vector and translation getters") -vector_result = t_test.vector() -translation_result = t_test.translation() -assert isapprox(vector_result[0], 10.0) -assert isapprox(translation_result[0], 10.0) - -verbose and print("[Translation] Inverse") -t_original = nanoeigenpy.Translation(np.array([2.0, 3.0, 4.0])) -t_inverse = t_original.inverse() -assert isapprox(t_inverse.x, -2.0) -assert isapprox(t_inverse.y, -3.0) -assert isapprox(t_inverse.z, -4.0) - -verbose and print("[Translation] Concatenation") -t1 = nanoeigenpy.Translation(np.array([1.0, 2.0, 3.0])) -t2 = nanoeigenpy.Translation(np.array([4.0, 5.0, 6.0])) -t_combined = t1 * t2 -assert isapprox(t_combined.x, 5.0) -assert isapprox(t_combined.y, 7.0) -assert isapprox(t_combined.z, 9.0) - -verbose and print("[Translation] isApprox") -t_approx1 = nanoeigenpy.Translation(np.array([1.0, 2.0, 3.0])) -t_approx2 = nanoeigenpy.Translation(np.array([1.0 + 1e-15, 2.0 + 1e-15, 3.0 + 1e-15])) -t_approx3 = nanoeigenpy.Translation(np.array([1.1, 2.1, 3.1])) -assert t_approx1.isApprox(t_approx2) -assert t_approx1.isApprox(t_approx2, 1e-12) -assert not t_approx1.isApprox(t_approx3) - -# --- JacobiRotation --------------------------------------------------------------- -verbose and print("[JacobiRotation] Default constructor") -j = nanoeigenpy.JacobiRotation() -assert hasattr(j, "c") -assert hasattr(j, "s") - -verbose and print("[JacobiRotation] Cosine-sine constructor") -c_val = 0.8 -s_val = 0.6 -j = nanoeigenpy.JacobiRotation(c_val, s_val) -assert isapprox(j.c, c_val) -assert isapprox(j.s, s_val) - -verbose and print("[JacobiRotation] Property access") -j.c = 0.8 -j.s = 0.6 -assert isapprox(j.c, 0.8) -assert isapprox(j.s, 0.6) -norm_squared = j.c**2 + j.s**2 -assert isapprox(norm_squared, 1.0, 1e-12) - -verbose and print("[JacobiRotation] Multiplication operator") -j1 = nanoeigenpy.JacobiRotation(0.8, 0.6) -j2 = nanoeigenpy.JacobiRotation(0.6, 0.8) -j_mult = j1 * j2 -assert hasattr(j_mult, "c") -assert hasattr(j_mult, "s") -norm_mult = j_mult.c**2 + j_mult.s**2 -assert isapprox(norm_mult, 1.0, 1e-12) - -verbose and print("[JacobiRotation] Transpose") -j = nanoeigenpy.JacobiRotation(0.8, 0.6) -j_t = j.transpose() -assert isapprox(j_t.c, j.c) -assert isapprox(j_t.s, -j.s) - -verbose and print("[JacobiRotation] Adjoint") -j = nanoeigenpy.JacobiRotation(0.8, 0.6) -j_adj = j.adjoint() -assert isapprox(j_adj.c, j.c) -assert isapprox(j_adj.s, -j.s) - -verbose and print("[JacobiRotation] Identity property") -j = nanoeigenpy.JacobiRotation(0.8, 0.6) -j_t = j.transpose() -identity = j * j_t -assert isapprox(identity.c, 1.0, 1e-12) -assert isapprox(identity.s, 0.0, 1e-12) - -verbose and print("[JacobiRotation] makeJacobi from scalars") -j = nanoeigenpy.JacobiRotation() -x, z = 4.0, 1.0 -y = 2.0 -result = j.makeJacobi(x, y, z) -assert isinstance(result, bool) -norm_after = j.c**2 + j.s**2 -assert isapprox(norm_after, 1.0, 1e-12) - -verbose and print("[JacobiRotation] makeJacobi from matrix") -M = np.array([[4.0, 2.0, 1.0], [2.0, 3.0, 0.5], [1.0, 0.5, 1.0]]) -j = nanoeigenpy.JacobiRotation() -result = j.makeJacobi(M, 0, 1) -assert isinstance(result, bool) -norm_matrix = j.c**2 + j.s**2 -assert isapprox(norm_matrix, 1.0, 1e-12) - -verbose and print("[JacobiRotation] makeGivens basic") -j = nanoeigenpy.JacobiRotation() -p_val = 3.0 -q_val = 4.0 -j.makeGivens(p_val, q_val) -norm_givens = j.c**2 + j.s**2 -assert isapprox(norm_givens, 1.0, 1e-12) - -verbose and print("[JacobiRotation] makeGivens with r parameter") -j = nanoeigenpy.JacobiRotation() -p_val = 3.0 -q_val = 4.0 -r_container = np.array([0.0]) -j.makeGivens(p_val, q_val, r_container.ctypes.data) -expected_r = np.sqrt(p_val**2 + q_val**2) - -verbose and print("[JacobiRotation] Edge cases") -j_zero = nanoeigenpy.JacobiRotation(1.0, 0.0) -assert isapprox(j_zero.c, 1.0) -assert isapprox(j_zero.s, 0.0) - -j_90 = nanoeigenpy.JacobiRotation(0.0, 1.0) -assert isapprox(j_90.c, 0.0) -assert isapprox(j_90.s, 1.0) - -j = nanoeigenpy.JacobiRotation() -j.makeGivens(5.0, 0.0) -assert isapprox(abs(j.c), 1.0) -assert isapprox(j.s, 0.0) - -j.makeGivens(0.0, 5.0) -assert isapprox(j.c, 0.0) -assert isapprox(abs(j.s), 1.0) - -verbose and print("[JacobiRotation] makeJacobi small off-diagonal") -j = nanoeigenpy.JacobiRotation() -result = j.makeJacobi(1.0, 1e-15, 2.0) -assert isinstance(result, bool) -if not result: - assert isapprox(j.c, 1.0) + distance = line_x_axis.distance(test_point) + squared_distance = line_x_axis.squaredDistance(test_point) + assert isapprox(distance, 0.0) + assert isapprox(squared_distance, 0.0) + + off_line_point = np.array([1.0, 1.0]) + distance_off = line_x_axis.distance(off_line_point) + squared_distance_off = line_x_axis.squaredDistance(off_line_point) + assert isapprox(distance_off, 1.0) + assert isapprox(squared_distance_off, 1.0) + assert isapprox(distance_off * distance_off, squared_distance_off) + + verbose and print("[ParametrizedLine] Projection") + projection = line_x_axis.projection(off_line_point) + assert isapprox(projection, np.array([1.0, 0.0])) + assert isapprox(line_x_axis.distance(projection), 0.0) + + verbose and print("[ParametrizedLine] Intersection with hyperplane") + line_diagonal = nanoeigenpy.ParametrizedLine( + np.array([0.0, 0.0]), np.array([1.0, 1.0]) + ) + h_vertical = nanoeigenpy.Hyperplane(np.array([1.0, 0.0]), -1.0) + + intersection_param = line_diagonal.intersectionParameter(h_vertical) + assert isapprox(intersection_param, 1.0) + + intersection_param_old = line_diagonal.intersection(h_vertical) + assert isapprox(intersection_param_old, intersection_param) + + intersection_point = line_diagonal.intersectionPoint(h_vertical) + expected_intersection = np.array([1.0, 1.0]) + assert isapprox(intersection_point, expected_intersection) + assert isapprox(h_vertical.absDistance(intersection_point), 0.0) + + verbose and print("[ParametrizedLine] isApprox") + line1 = nanoeigenpy.ParametrizedLine(np.array([0.0, 0.0]), np.array([1.0, 0.0])) + line2 = nanoeigenpy.ParametrizedLine(np.array([0.0, 0.0]), np.array([1.0, 0.0])) + line3 = nanoeigenpy.ParametrizedLine(np.array([0.0, 0.0]), np.array([0.0, 1.0])) + assert line1.isApprox(line2) + assert line1.isApprox(line2, 1e-12) + assert not line1.isApprox(line3) + + verbose and print("[ParametrizedLine] Parallel lines") + line_parallel1 = nanoeigenpy.ParametrizedLine( + np.array([0.0, 0.0]), np.array([1.0, 0.0]) + ) + line_parallel2 = nanoeigenpy.ParametrizedLine( + np.array([0.0, 1.0]), np.array([1.0, 0.0]) + ) + assert not line_parallel1.isApprox(line_parallel2) + + test_points = [np.array([i, 0.0]) for i in range(5)] + distances = [line_parallel2.distance(p) for p in test_points] + for d in distances: + assert isapprox(d, 1.0) + + verbose and print("[ParametrizedLine] Through two points") + p0 = np.array([0.0, 0.0]) + p1 = np.array([1.0, 1.0]) + line_through = nanoeigenpy.ParametrizedLine.Through(p0, p1) + direction = line_through.direction() + expected_dir = (p1 - p0) / np.linalg.norm(p1 - p0) + assert isapprox(line_through.origin(), p0) + assert isapprox(np.linalg.norm(direction), 1.0) + assert isapprox(direction, expected_dir) + + # --- Rotation2D ------------------------------------------------ + verbose and print("[Rotation2D] Default constructor") + r_default = nanoeigenpy.Rotation2D() + assert isapprox(r_default.angle, 0.0) + + verbose and print("[Rotation2D] Angle constructor") + angle = np.pi / 4 + r_angle = nanoeigenpy.Rotation2D(angle) + assert isapprox(r_angle.angle, angle) + + verbose and print("[Rotation2D] Copy constructor") + r_copy = nanoeigenpy.Rotation2D(r_angle) + assert isapprox(r_copy.angle, r_angle.angle) + assert r_copy == r_angle + + verbose and print("[Rotation2D] Matrix constructor") + theta = np.pi / 6 + rotation_matrix = np.array( + [[np.cos(theta), -np.sin(theta)], [np.sin(theta), np.cos(theta)]] + ) + r_matrix = nanoeigenpy.Rotation2D(rotation_matrix) + assert isapprox(r_matrix.angle, theta) + + verbose and print("[Rotation2D] Angle property") + r_prop = nanoeigenpy.Rotation2D() + new_angle = np.pi / 3 + r_prop.angle = new_angle + assert isapprox(r_prop.angle, new_angle) + + verbose and print("[Rotation2D] smallestPositiveAngle") + r_negative = nanoeigenpy.Rotation2D(-np.pi / 4) + positive_angle = r_negative.smallestPositiveAngle() + assert positive_angle >= 0.0 + assert positive_angle < 2 * np.pi + assert isapprox(positive_angle, 7 * np.pi / 4) + + verbose and print("[Rotation2D] smallestAngle") + r_large = nanoeigenpy.Rotation2D(3 * np.pi) + smallest_angle = r_large.smallestAngle() + assert smallest_angle >= -np.pi + assert smallest_angle <= np.pi + assert isapprox(smallest_angle, np.pi) + + verbose and print("[Rotation2D] Identity") + r_identity = nanoeigenpy.Rotation2D.Identity() + assert isapprox(r_identity.angle, 0.0) + + verbose and print("[Rotation2D] fromRotationMatrix") + r_from_matrix = nanoeigenpy.Rotation2D() + theta2 = np.pi / 2 + matrix2 = np.array( + [[np.cos(theta2), -np.sin(theta2)], [np.sin(theta2), np.cos(theta2)]] + ) + r_from_matrix.fromRotationMatrix(matrix2) + assert isapprox(r_from_matrix.angle, theta2) + + verbose and print("[Rotation2D] Rotation composition") + r1 = nanoeigenpy.Rotation2D(np.pi / 4) + r2 = nanoeigenpy.Rotation2D(np.pi / 6) + r_composed = r1 * r2 + expected_angle = np.pi / 4 + np.pi / 6 + assert isapprox(r_composed.angle, expected_angle) + + verbose and print("[Rotation2D] In-place multiplication") + r_inplace = nanoeigenpy.Rotation2D(np.pi / 4) + original_angle = r_inplace.angle + r_inplace *= nanoeigenpy.Rotation2D(np.pi / 6) + assert isapprox(r_inplace.angle, original_angle + np.pi / 6) + + verbose and print("[Rotation2D] Vector rotation") + r_90 = nanoeigenpy.Rotation2D(np.pi / 2) + vec = np.array([1.0, 0.0]) + rotated_vec = r_90 * vec + expected_vec = np.array([0.0, 1.0]) + assert isapprox(rotated_vec, expected_vec) + + vec2 = np.array([1.0, 1.0]) + r_45 = nanoeigenpy.Rotation2D(np.pi / 4) + rotated_vec2 = r_45 * vec2 + expected_vec2 = np.array([0.0, np.sqrt(2)]) + assert isapprox(rotated_vec2, expected_vec2) + + verbose and print("[Rotation2D] Equality operators") + r_eq1 = nanoeigenpy.Rotation2D(np.pi / 3) + r_eq2 = nanoeigenpy.Rotation2D(np.pi / 3) + r_eq3 = nanoeigenpy.Rotation2D(np.pi / 4) + + assert r_eq1 == r_eq2 + assert not (r_eq1 == r_eq3) + assert r_eq1 != r_eq3 + assert not (r_eq1 != r_eq2) + + verbose and print("[Rotation2D] Periodic angles") + r_period1 = nanoeigenpy.Rotation2D(0.0) # noqa + r_period2 = nanoeigenpy.Rotation2D(2 * np.pi) # noqa + verbose and print("[Rotation2D] isApprox") + r_approx1 = nanoeigenpy.Rotation2D(np.pi / 4) + r_approx2 = nanoeigenpy.Rotation2D(np.pi / 4 + 1e-15) + r_approx3 = nanoeigenpy.Rotation2D(np.pi / 3) + + assert r_approx1.isApprox(r_approx2) + assert r_approx1.isApprox(r_approx2, 1e-12) + assert not r_approx1.isApprox(r_approx3) + + verbose and print("[Rotation2D] slerp") + r_start = nanoeigenpy.Rotation2D(0.0) + r_end = nanoeigenpy.Rotation2D(np.pi / 2) + r_middle = r_start.slerp(0.5, r_end) + assert isapprox(r_middle.angle, np.pi / 4) + + r_slerp_0 = r_start.slerp(0.0, r_end) + r_slerp_1 = r_start.slerp(1.0, r_end) + assert isapprox(r_slerp_0.angle, r_start.angle) + assert isapprox(r_slerp_1.angle, r_end.angle) + + verbose and print("[Rotation2D] Inverse rotation") + try: + r_original = nanoeigenpy.Rotation2D(np.pi / 3) + r_inverse = r_original.inverse() + assert isapprox(r_inverse.angle, -np.pi / 3) + + r_identity_test = r_original * r_inverse + assert isapprox(r_identity_test.angle, 0.0, 1e-12) + except AttributeError: + if verbose: + print("inverse() method not exposed or not available") + + verbose and print("[Rotation2D] Matrix conversion") + try: + r_matrix_test = nanoeigenpy.Rotation2D(np.pi / 6) + matrix = r_matrix_test.matrix() + + assert matrix.shape == (2, 2) + assert isapprox(matrix @ matrix.T, np.eye(2)) + assert isapprox(np.linalg.det(matrix), 1.0) + + expected_matrix = np.array( + [ + [np.cos(np.pi / 6), -np.sin(np.pi / 6)], + [np.sin(np.pi / 6), np.cos(np.pi / 6)], + ] + ) + assert isapprox(matrix, expected_matrix) + + except AttributeError: + if verbose: + print("matrix() method not exposed or not available") + + verbose and print("[Rotation2D] Angle normalization") + r_large_angle = nanoeigenpy.Rotation2D(3 * np.pi) + vec_test = np.array([1.0, 0.0]) + rotated_large = r_large_angle * vec_test + expected_large = np.array([-1.0, 0.0]) + assert isapprox(rotated_large, expected_large) + + # --- UniformScaling ------------------------------------------------ + verbose and print("[UniformScaling] Default constructor") + s_default = nanoeigenpy.UniformScaling() # noqa + + verbose and print("[UniformScaling] Factor constructor") + factor = 2.5 + s_factor = nanoeigenpy.UniformScaling(factor) + assert isapprox(s_factor.factor(), factor) + + verbose and print("[UniformScaling] Copy constructor") + s_copy = nanoeigenpy.UniformScaling(s_factor) + assert isapprox(s_copy.factor(), s_factor.factor()) + + verbose and print("[UniformScaling] Factor getter") + s_test = nanoeigenpy.UniformScaling(3.0) + assert isapprox(s_test.factor(), 3.0) + + verbose and print("[UniformScaling] Inverse scaling") + s_original = nanoeigenpy.UniformScaling(4.0) + s_inverse = s_original.inverse() + assert isapprox(s_inverse.factor(), 1.0 / 4.0) + + s_identity_test = s_original * s_inverse + assert isapprox(s_identity_test.factor(), 1.0) + + verbose and print("[UniformScaling] Concatenation of scalings") + s1 = nanoeigenpy.UniformScaling(2.0) + s2 = nanoeigenpy.UniformScaling(3.0) + s_combined = s1 * s2 + assert isapprox(s_combined.factor(), 6.0) + + verbose and print("[UniformScaling] Multiplication with matrix") + s_scale = nanoeigenpy.UniformScaling(2.0) + matrix = np.array([[1.0, 2.0], [3.0, 4.0]]) + scaled_matrix = s_scale * matrix + expected_matrix = matrix * 2.0 + assert isapprox(scaled_matrix, expected_matrix) + + identity = np.eye(3) + s_identity_scale = nanoeigenpy.UniformScaling(5.0) + scaled_identity = s_identity_scale * identity + expected_identity = identity * 5.0 + assert isapprox(scaled_identity, expected_identity) + + verbose and print("[UniformScaling] Multiplication with AngleAxis") + try: + angle_axis = nanoeigenpy.AngleAxis(np.pi / 4, np.array([0.0, 0.0, 1.0])) + s_with_rotation = nanoeigenpy.UniformScaling(2.0) + result_rotation = s_with_rotation * angle_axis + + assert result_rotation.shape == (3, 3) + det = np.linalg.det(result_rotation) + assert isapprox(det, 2.0**3) + + except (AttributeError, NameError): + if verbose: + print("AngleAxis class not available or not exposed") + + verbose and print("[UniformScaling] Multiplication with Quaternion") + try: + quat = nanoeigenpy.Quaternion(1, 0, 0, 0) + s_with_quat = nanoeigenpy.UniformScaling(3.0) + result_quat = s_with_quat * quat + + assert result_quat.shape == (3, 3) + expected_scaled_identity = np.eye(3) * 3.0 + assert isapprox(result_quat, expected_scaled_identity) + + except (AttributeError, NameError): + if verbose: + print("Quaternion class not available or not exposed") + + verbose and print("[UniformScaling] Multiplication with Rotation2D") + try: + rotation_2d = nanoeigenpy.Rotation2D(np.pi / 4) + s_with_rot2d = nanoeigenpy.UniformScaling(2.0) + result_rot2d = s_with_rot2d * rotation_2d + + assert result_rot2d.shape == (2, 2) + det_2d = np.linalg.det(result_rot2d) + assert isapprox(det_2d, 2.0**2) + + except (AttributeError, NameError): + if verbose: + print("Rotation2D class not available or not exposed") + + verbose and print("[UniformScaling] isApprox") + s_approx1 = nanoeigenpy.UniformScaling(2.0) + s_approx2 = nanoeigenpy.UniformScaling(2.0 + 1e-15) + s_approx3 = nanoeigenpy.UniformScaling(3.0) + + assert s_approx1.isApprox(s_approx2) + assert s_approx1.isApprox(s_approx2, 1e-12) + assert not s_approx1.isApprox(s_approx3) + + verbose and print("[UniformScaling] Edge cases") + s_zero = nanoeigenpy.UniformScaling(0.0) + assert isapprox(s_zero.factor(), 0.0) + try: + s_zero_inverse = s_zero.inverse() + if verbose: + print("Zero scaling inverse:", s_zero_inverse.factor()) + except Exception as e: + if verbose: + print("Zero scaling inverse threw exception (expected):", type(e).__name__) + + s_negative = nanoeigenpy.UniformScaling(-2.0) + assert isapprox(s_negative.factor(), -2.0) + s_negative_inverse = s_negative.inverse() + assert isapprox(s_negative_inverse.factor(), -0.5) + + s_small = nanoeigenpy.UniformScaling(1e-10) + s_small_inverse = s_small.inverse() + assert isapprox(s_small_inverse.factor(), 1e10) + + verbose and print("[UniformScaling] Chain operations") + s_chain1 = nanoeigenpy.UniformScaling(2.0) + s_chain2 = nanoeigenpy.UniformScaling(3.0) + s_chain3 = nanoeigenpy.UniformScaling(4.0) + + left_assoc = (s_chain1 * s_chain2) * s_chain3 + right_assoc = s_chain1 * (s_chain2 * s_chain3) + assert isapprox(left_assoc.factor(), right_assoc.factor()) + assert isapprox(left_assoc.factor(), 24.0) + + verbose and print("[UniformScaling] Vector scaling") + s_vector = nanoeigenpy.UniformScaling(2.0) + vector = np.array([[1.0], [2.0], [3.0]]) + identity_3x3 = np.eye(3) + scaled_identity = s_vector * identity_3x3 + scaled_vector = scaled_identity @ vector + expected_vector = vector * 2.0 + assert isapprox(scaled_vector, expected_vector) + + # --- Translation ------------------------------------------------ + verbose and print("[Translation] Default constructor") + t_default = nanoeigenpy.Translation() # noqa + + verbose and print("[Translation] 2D constructor with vector") + t_2d = nanoeigenpy.Translation(np.array([1.0, 2.0])) + assert isapprox(t_2d.x, 1.0) + assert isapprox(t_2d.y, 2.0) + + verbose and print("[Translation] 3D constructor with vector") + t_3d = nanoeigenpy.Translation(np.array([1.0, 2.0, 3.0])) + assert isapprox(t_3d.x, 1.0) + assert isapprox(t_3d.y, 2.0) + assert isapprox(t_3d.z, 3.0) + + verbose and print("[Translation] Vector constructor") + vector = np.array([1.5, 2.5, 3.5]) + t_vector = nanoeigenpy.Translation(vector) + assert isapprox(t_vector.x, 1.5) + assert isapprox(t_vector.y, 2.5) + assert isapprox(t_vector.z, 3.5) + + verbose and print("[Translation] Copy constructor") + t_copy = nanoeigenpy.Translation(t_3d) + assert isapprox(t_copy.x, t_3d.x) + assert isapprox(t_copy.y, t_3d.y) + assert isapprox(t_copy.z, t_3d.z) + + verbose and print("[Translation] Property setters") + t_test = nanoeigenpy.Translation(np.array([1.0, 2.0, 3.0])) + t_test.x = 10.0 + t_test.y = 20.0 + t_test.z = 30.0 + assert isapprox(t_test.x, 10.0) + assert isapprox(t_test.y, 20.0) + assert isapprox(t_test.z, 30.0) + + verbose and print("[Translation] Vector and translation getters") + vector_result = t_test.vector() + translation_result = t_test.translation() + assert isapprox(vector_result[0], 10.0) + assert isapprox(translation_result[0], 10.0) + + verbose and print("[Translation] Inverse") + t_original = nanoeigenpy.Translation(np.array([2.0, 3.0, 4.0])) + t_inverse = t_original.inverse() + assert isapprox(t_inverse.x, -2.0) + assert isapprox(t_inverse.y, -3.0) + assert isapprox(t_inverse.z, -4.0) + + verbose and print("[Translation] Concatenation") + t1 = nanoeigenpy.Translation(np.array([1.0, 2.0, 3.0])) + t2 = nanoeigenpy.Translation(np.array([4.0, 5.0, 6.0])) + t_combined = t1 * t2 + assert isapprox(t_combined.x, 5.0) + assert isapprox(t_combined.y, 7.0) + assert isapprox(t_combined.z, 9.0) + + verbose and print("[Translation] isApprox") + t_approx1 = nanoeigenpy.Translation(np.array([1.0, 2.0, 3.0])) + t_approx2 = nanoeigenpy.Translation( + np.array([1.0 + 1e-15, 2.0 + 1e-15, 3.0 + 1e-15]) + ) + t_approx3 = nanoeigenpy.Translation(np.array([1.1, 2.1, 3.1])) + assert t_approx1.isApprox(t_approx2) + assert t_approx1.isApprox(t_approx2, 1e-12) + assert not t_approx1.isApprox(t_approx3) + + # --- JacobiRotation --------------------------------------------------------------- + verbose and print("[JacobiRotation] Default constructor") + j = nanoeigenpy.JacobiRotation() + assert hasattr(j, "c") + assert hasattr(j, "s") + + verbose and print("[JacobiRotation] Cosine-sine constructor") + c_val = 0.8 + s_val = 0.6 + j = nanoeigenpy.JacobiRotation(c_val, s_val) + assert isapprox(j.c, c_val) + assert isapprox(j.s, s_val) + + verbose and print("[JacobiRotation] Property access") + j.c = 0.8 + j.s = 0.6 + assert isapprox(j.c, 0.8) + assert isapprox(j.s, 0.6) + norm_squared = j.c**2 + j.s**2 + assert isapprox(norm_squared, 1.0, 1e-12) + + verbose and print("[JacobiRotation] Multiplication operator") + j1 = nanoeigenpy.JacobiRotation(0.8, 0.6) + j2 = nanoeigenpy.JacobiRotation(0.6, 0.8) + j_mult = j1 * j2 + assert hasattr(j_mult, "c") + assert hasattr(j_mult, "s") + norm_mult = j_mult.c**2 + j_mult.s**2 + assert isapprox(norm_mult, 1.0, 1e-12) + + verbose and print("[JacobiRotation] Transpose") + j = nanoeigenpy.JacobiRotation(0.8, 0.6) + j_t = j.transpose() + assert isapprox(j_t.c, j.c) + assert isapprox(j_t.s, -j.s) + + verbose and print("[JacobiRotation] Adjoint") + j = nanoeigenpy.JacobiRotation(0.8, 0.6) + j_adj = j.adjoint() + assert isapprox(j_adj.c, j.c) + assert isapprox(j_adj.s, -j.s) + + verbose and print("[JacobiRotation] Identity property") + j = nanoeigenpy.JacobiRotation(0.8, 0.6) + j_t = j.transpose() + identity = j * j_t + assert isapprox(identity.c, 1.0, 1e-12) + assert isapprox(identity.s, 0.0, 1e-12) + + verbose and print("[JacobiRotation] makeJacobi from scalars") + j = nanoeigenpy.JacobiRotation() + x, z = 4.0, 1.0 + y = 2.0 + result = j.makeJacobi(x, y, z) + assert isinstance(result, bool) + norm_after = j.c**2 + j.s**2 + assert isapprox(norm_after, 1.0, 1e-12) + + verbose and print("[JacobiRotation] makeJacobi from matrix") + M = np.array([[4.0, 2.0, 1.0], [2.0, 3.0, 0.5], [1.0, 0.5, 1.0]]) + j = nanoeigenpy.JacobiRotation() + result = j.makeJacobi(M, 0, 1) + assert isinstance(result, bool) + norm_matrix = j.c**2 + j.s**2 + assert isapprox(norm_matrix, 1.0, 1e-12) + + verbose and print("[JacobiRotation] makeGivens basic") + j = nanoeigenpy.JacobiRotation() + p_val = 3.0 + q_val = 4.0 + j.makeGivens(p_val, q_val) + norm_givens = j.c**2 + j.s**2 + assert isapprox(norm_givens, 1.0, 1e-12) + + verbose and print("[JacobiRotation] makeGivens with r parameter") + j = nanoeigenpy.JacobiRotation() + p_val = 3.0 + q_val = 4.0 + r_container = np.array([0.0]) + j.makeGivens(p_val, q_val, r_container.ctypes.data) + expected_r = np.sqrt(p_val**2 + q_val**2) # noqa + + verbose and print("[JacobiRotation] Edge cases") + j_zero = nanoeigenpy.JacobiRotation(1.0, 0.0) + assert isapprox(j_zero.c, 1.0) + assert isapprox(j_zero.s, 0.0) + + j_90 = nanoeigenpy.JacobiRotation(0.0, 1.0) + assert isapprox(j_90.c, 0.0) + assert isapprox(j_90.s, 1.0) + + j = nanoeigenpy.JacobiRotation() + j.makeGivens(5.0, 0.0) + assert isapprox(abs(j.c), 1.0) assert isapprox(j.s, 0.0) + + j.makeGivens(0.0, 5.0) + assert isapprox(j.c, 0.0) + assert isapprox(abs(j.s), 1.0) + + verbose and print("[JacobiRotation] makeJacobi small off-diagonal") + j = nanoeigenpy.JacobiRotation() + result = j.makeJacobi(1.0, 1e-15, 2.0) + assert isinstance(result, bool) + if not result: + assert isapprox(j.c, 1.0) + assert isapprox(j.s, 0.0) diff --git a/tests/test_hessenberg_decomposition.py b/tests/test_hessenberg_decomposition.py index 9a09802..a82d8d8 100644 --- a/tests/test_hessenberg_decomposition.py +++ b/tests/test_hessenberg_decomposition.py @@ -1,70 +1,72 @@ import nanoeigenpy import numpy as np -dim = 100 -rng = np.random.default_rng() -A = rng.random((dim, dim)) - -hess = nanoeigenpy.HessenbergDecomposition(A) - -Q = hess.matrixQ() -H = hess.matrixH() - -if np.iscomplexobj(A): - A_reconstructed = Q @ H @ Q.conj().T -else: - A_reconstructed = Q @ H @ Q.T -assert nanoeigenpy.is_approx(A, A_reconstructed) - -for row in range(2, dim): - for col in range(row - 1): - assert abs(H[row, col]) < 1e-12 - -if np.iscomplexobj(Q): - QQ_conj = Q @ Q.conj().T -else: - QQ_conj = Q @ Q.T -assert nanoeigenpy.is_approx(QQ_conj, np.eye(dim)) - -A_test = rng.random((dim, dim)) -hess1 = nanoeigenpy.HessenbergDecomposition(dim) -hess1.compute(A_test) -hess2 = nanoeigenpy.HessenbergDecomposition(A_test) - -H1 = hess1.matrixH() -H2 = hess2.matrixH() -Q1 = hess1.matrixQ() -Q2 = hess2.matrixQ() - -assert nanoeigenpy.is_approx(H1, H2) -assert nanoeigenpy.is_approx(Q1, Q2) - -hCoeffs = hess.householderCoefficients() -packed = hess.packedMatrix() - -assert hCoeffs.shape == (dim - 1,) -assert packed.shape == (dim, dim) - -for i in range(dim): - for j in range(i - 1, dim): - if j >= 0: - assert abs(H[i, j] - packed[i, j]) < 1e-12 - -hess_default = nanoeigenpy.HessenbergDecomposition(dim) -hess_matrix = nanoeigenpy.HessenbergDecomposition(A) - -hess1_id = nanoeigenpy.HessenbergDecomposition(dim) -hess2_id = nanoeigenpy.HessenbergDecomposition(dim) -id1 = hess1_id.id() -id2 = hess2_id.id() -assert id1 != id2 -assert id1 == hess1_id.id() -assert id2 == hess2_id.id() - -hess3_id = nanoeigenpy.HessenbergDecomposition(A) -hess4_id = nanoeigenpy.HessenbergDecomposition(A) -id3 = hess3_id.id() -id4 = hess4_id.id() -assert id3 != id4 -assert id3 == hess3_id.id() -assert id4 == hess4_id.id() + +def test_hessenberg_decomposition(): + dim = 100 + rng = np.random.default_rng() + A = rng.random((dim, dim)) + + hess = nanoeigenpy.HessenbergDecomposition(A) + + Q = hess.matrixQ() + H = hess.matrixH() + + if np.iscomplexobj(A): + A_reconstructed = Q @ H @ Q.conj().T + else: + A_reconstructed = Q @ H @ Q.T + assert nanoeigenpy.is_approx(A, A_reconstructed) + + for row in range(2, dim): + for col in range(row - 1): + assert abs(H[row, col]) < 1e-12 + + if np.iscomplexobj(Q): + QQ_conj = Q @ Q.conj().T + else: + QQ_conj = Q @ Q.T + assert nanoeigenpy.is_approx(QQ_conj, np.eye(dim)) + + A_test = rng.random((dim, dim)) + hess1 = nanoeigenpy.HessenbergDecomposition(dim) + hess1.compute(A_test) + hess2 = nanoeigenpy.HessenbergDecomposition(A_test) + + H1 = hess1.matrixH() + H2 = hess2.matrixH() + Q1 = hess1.matrixQ() + Q2 = hess2.matrixQ() + + assert nanoeigenpy.is_approx(H1, H2) + assert nanoeigenpy.is_approx(Q1, Q2) + + hCoeffs = hess.householderCoefficients() + packed = hess.packedMatrix() + + assert hCoeffs.shape == (dim - 1,) + assert packed.shape == (dim, dim) + + for i in range(dim): + for j in range(i - 1, dim): + if j >= 0: + assert abs(H[i, j] - packed[i, j]) < 1e-12 + + hess_default = nanoeigenpy.HessenbergDecomposition(dim) # noqa + hess_matrix = nanoeigenpy.HessenbergDecomposition(A) # noqa + + hess1_id = nanoeigenpy.HessenbergDecomposition(dim) + hess2_id = nanoeigenpy.HessenbergDecomposition(dim) + id1 = hess1_id.id() + id2 = hess2_id.id() + assert id1 != id2 + assert id1 == hess1_id.id() + assert id2 == hess2_id.id() + + hess3_id = nanoeigenpy.HessenbergDecomposition(A) + hess4_id = nanoeigenpy.HessenbergDecomposition(A) + id3 = hess3_id.id() + id4 = hess4_id.id() + assert id3 != id4 + assert id3 == hess3_id.id() + assert id4 == hess4_id.id() diff --git a/tests/test_import_extension.py b/tests/test_import_extension.py new file mode 100644 index 0000000..56b01bb --- /dev/null +++ b/tests/test_import_extension.py @@ -0,0 +1,5 @@ +import nanoeigenpy + + +def test_import_nanoeigenpy(): + assert hasattr(nanoeigenpy, "EigenSolver") diff --git a/tests/test_incomplete_cholesky.py b/tests/test_incomplete_cholesky.py index 01f4f11..3bd74b0 100644 --- a/tests/test_incomplete_cholesky.py +++ b/tests/test_incomplete_cholesky.py @@ -1,71 +1,72 @@ +import nanoeigenpy import numpy as np from scipy.sparse import csc_matrix -import nanoeigenpy -dim = 100 -rng = np.random.default_rng() - -A = rng.random((dim, dim)) -A = (A + A.T) * 0.5 + np.diag(5.0 + rng.random(dim)) -A = csc_matrix(A) - -ichol = nanoeigenpy.solvers.IncompleteCholesky(A) -assert ichol.info() == nanoeigenpy.ComputationInfo.Success -assert ichol.rows() == dim -assert ichol.cols() == dim - -X = rng.random((dim, 20)) -B = A.dot(X) -X_est = ichol.solve(B) -assert isinstance(X_est, np.ndarray) -residual = np.linalg.norm(B - A.dot(X_est)) / np.linalg.norm(B) -assert residual < 0.1 - -x = rng.random(dim) -b = A.dot(x) -x_est = ichol.solve(b) -assert isinstance(x_est, np.ndarray) -residual = np.linalg.norm(b - A.dot(x_est)) / np.linalg.norm(b) -assert residual < 0.1 - -X_sparse = csc_matrix(rng.random((dim, 10))) -B_sparse = A.dot(X_sparse).tocsc() -if not B_sparse.has_sorted_indices: - B_sparse.sort_indices() -X_est_sparse = ichol.solve(B_sparse) -assert isinstance(X_est_sparse, csc_matrix) - -ichol.analyzePattern(A) -ichol.factorize(A) -ichol.compute(A) -assert ichol.info() == nanoeigenpy.ComputationInfo.Success - -L = ichol.matrixL() -S_diag = ichol.scalingS() -perm = ichol.permutationP() -P = perm.toDenseMatrix() - -assert isinstance(L, csc_matrix) -assert isinstance(S_diag, np.ndarray) -assert L.shape == (dim, dim) -assert S_diag.shape == (dim,) - -L_dense = L.toarray() -upper_part = np.triu(L_dense, k=1) -assert np.allclose(upper_part, 0, atol=1e-12) - -assert np.all(S_diag > 0) - -S = csc_matrix((S_diag, (range(dim), range(dim))), shape=(dim, dim)) - -PA = P @ A -PAP = PA @ P.T -SPAP = S @ PAP -SPAPS = SPAP @ S - -LLT = L @ L.T - -diff = SPAPS - LLT -relative_error = np.linalg.norm(diff.data) / np.linalg.norm(SPAPS.data) -assert relative_error < 0.5 +def test_incomplete_cholesky(): + dim = 100 + rng = np.random.default_rng() + + A = rng.random((dim, dim)) + A = (A + A.T) * 0.5 + np.diag(5.0 + rng.random(dim)) + A = csc_matrix(A) + + ichol = nanoeigenpy.solvers.IncompleteCholesky(A) + assert ichol.info() == nanoeigenpy.ComputationInfo.Success + assert ichol.rows() == dim + assert ichol.cols() == dim + + X = rng.random((dim, 20)) + B = A.dot(X) + X_est = ichol.solve(B) + assert isinstance(X_est, np.ndarray) + residual = np.linalg.norm(B - A.dot(X_est)) / np.linalg.norm(B) + assert residual < 0.1 + + x = rng.random(dim) + b = A.dot(x) + x_est = ichol.solve(b) + assert isinstance(x_est, np.ndarray) + residual = np.linalg.norm(b - A.dot(x_est)) / np.linalg.norm(b) + assert residual < 0.1 + + X_sparse = csc_matrix(rng.random((dim, 10))) + B_sparse = A.dot(X_sparse).tocsc() + if not B_sparse.has_sorted_indices: + B_sparse.sort_indices() + X_est_sparse = ichol.solve(B_sparse) + assert isinstance(X_est_sparse, csc_matrix) + + ichol.analyzePattern(A) + ichol.factorize(A) + ichol.compute(A) + assert ichol.info() == nanoeigenpy.ComputationInfo.Success + + L = ichol.matrixL() + S_diag = ichol.scalingS() + perm = ichol.permutationP() + P = perm.toDenseMatrix() + + assert isinstance(L, csc_matrix) + assert isinstance(S_diag, np.ndarray) + assert L.shape == (dim, dim) + assert S_diag.shape == (dim,) + + L_dense = L.toarray() + upper_part = np.triu(L_dense, k=1) + assert np.allclose(upper_part, 0, atol=1e-12) + + assert np.all(S_diag > 0) + + S = csc_matrix((S_diag, (range(dim), range(dim))), shape=(dim, dim)) + + PA = P @ A + PAP = PA @ P.T + SPAP = S @ PAP + SPAPS = SPAP @ S + + LLT = L @ L.T + + diff = SPAPS - LLT + relative_error = np.linalg.norm(diff.data) / np.linalg.norm(SPAPS.data) + assert relative_error < 0.5 diff --git a/tests/test_incomplete_lut.py b/tests/test_incomplete_lut.py index 4405e93..16afeb2 100644 --- a/tests/test_incomplete_lut.py +++ b/tests/test_incomplete_lut.py @@ -1,49 +1,51 @@ +import nanoeigenpy import numpy as np from scipy.sparse import csc_matrix -import nanoeigenpy -dim = 100 -rng = np.random.default_rng() - -A = rng.random((dim, dim)) -A = (A + A.T) * 0.5 + np.diag(5.0 + rng.random(dim)) -A = csc_matrix(A) - -ilut = nanoeigenpy.solvers.IncompleteLUT(A) -assert ilut.info() == nanoeigenpy.ComputationInfo.Success -assert ilut.rows() == dim -assert ilut.cols() == dim - -X = rng.random((dim, 100)) -B = A.dot(X) -X_est = ilut.solve(B) -assert isinstance(X_est, np.ndarray) -residual = np.linalg.norm(B - A.dot(X_est)) / np.linalg.norm(B) -assert residual < 0.1 - -x = rng.random(dim) -b = A.dot(x) -x_est = ilut.solve(b) -assert isinstance(x_est, np.ndarray) -residual = np.linalg.norm(b - A.dot(x_est)) / np.linalg.norm(b) -assert residual < 0.1 - -X_sparse = csc_matrix(rng.random((dim, 10))) -B_sparse = A.dot(X_sparse).tocsc() -if not B_sparse.has_sorted_indices: - B_sparse.sort_indices() -X_est_sparse = ilut.solve(B_sparse) -assert isinstance(X_est_sparse, csc_matrix) - -ilut.analyzePattern(A) -ilut.factorize(A) -assert ilut.info() == nanoeigenpy.ComputationInfo.Success - -ilut_params = nanoeigenpy.solvers.IncompleteLUT(A, 1e-4, 15) -assert ilut_params.info() == nanoeigenpy.ComputationInfo.Success - -ilut_set = nanoeigenpy.solvers.IncompleteLUT() -ilut_set.setDroptol(1e-3) -ilut_set.setFillfactor(20) -ilut_set.compute(A) -assert ilut_set.info() == nanoeigenpy.ComputationInfo.Success + +def test_incomplete_lut(): + dim = 100 + rng = np.random.default_rng() + + A = rng.random((dim, dim)) + A = (A + A.T) * 0.5 + np.diag(5.0 + rng.random(dim)) + A = csc_matrix(A) + + ilut = nanoeigenpy.solvers.IncompleteLUT(A) + assert ilut.info() == nanoeigenpy.ComputationInfo.Success + assert ilut.rows() == dim + assert ilut.cols() == dim + + X = rng.random((dim, 100)) + B = A.dot(X) + X_est = ilut.solve(B) + assert isinstance(X_est, np.ndarray) + residual = np.linalg.norm(B - A.dot(X_est)) / np.linalg.norm(B) + assert residual < 0.1 + + x = rng.random(dim) + b = A.dot(x) + x_est = ilut.solve(b) + assert isinstance(x_est, np.ndarray) + residual = np.linalg.norm(b - A.dot(x_est)) / np.linalg.norm(b) + assert residual < 0.1 + + X_sparse = csc_matrix(rng.random((dim, 10))) + B_sparse = A.dot(X_sparse).tocsc() + if not B_sparse.has_sorted_indices: + B_sparse.sort_indices() + X_est_sparse = ilut.solve(B_sparse) + assert isinstance(X_est_sparse, csc_matrix) + + ilut.analyzePattern(A) + ilut.factorize(A) + assert ilut.info() == nanoeigenpy.ComputationInfo.Success + + ilut_params = nanoeigenpy.solvers.IncompleteLUT(A, 1e-4, 15) + assert ilut_params.info() == nanoeigenpy.ComputationInfo.Success + + ilut_set = nanoeigenpy.solvers.IncompleteLUT() + ilut_set.setDroptol(1e-3) + ilut_set.setFillfactor(20) + ilut_set.compute(A) + assert ilut_set.info() == nanoeigenpy.ComputationInfo.Success diff --git a/tests/test_jacobi_svd.py b/tests/test_jacobi_svd.py index 8d0f066..dd14c39 100644 --- a/tests/test_jacobi_svd.py +++ b/tests/test_jacobi_svd.py @@ -111,9 +111,3 @@ def test_jacobi(cls, options): S_matrix = np.diag(S) A_reconstructed = U @ S_matrix @ V.T assert nanoeigenpy.is_approx(A, A_reconstructed) - - -if __name__ == "__main__": - import sys - - sys.exit(pytest.main(sys.argv)) diff --git a/tests/test_ldlt.py b/tests/test_ldlt.py index d844863..9a2484b 100644 --- a/tests/test_ldlt.py +++ b/tests/test_ldlt.py @@ -1,96 +1,98 @@ import nanoeigenpy import numpy as np -dim = 100 -rng = np.random.default_rng() - -A_neg = -np.eye(dim) -ldlt_neg = nanoeigenpy.LDLT(A_neg) -assert ldlt_neg.isNegative() -assert not ldlt_neg.isPositive() - -A_pos = np.eye(dim) -ldlt_pos = nanoeigenpy.LDLT(A_pos) -assert ldlt_pos.isPositive() -assert not ldlt_pos.isNegative() - -A = rng.random((dim, dim)) -A = (A + A.T) * 0.5 + np.diag(10.0 + rng.random(dim)) - -ldlt = nanoeigenpy.LDLT(A) -assert ldlt.info() == nanoeigenpy.ComputationInfo.Success - -L = ldlt.matrixL() -D = ldlt.vectorD() -P = ldlt.transpositionsP() -assert nanoeigenpy.is_approx( - np.transpose(P).dot(L.dot(np.diag(D).dot(np.transpose(L).dot(P)))), A -) - -X = rng.random((dim, 20)) -B = A.dot(X) -X_est = ldlt.solve(B) -assert nanoeigenpy.is_approx(X, X_est) -assert nanoeigenpy.is_approx(A.dot(X_est), B) - -x = rng.random(dim) -b = A.dot(x) -x_est = ldlt.solve(b) -assert nanoeigenpy.is_approx(x, x_est) -assert nanoeigenpy.is_approx(A.dot(x_est), b) - -A_reconstructed = ldlt.reconstructedMatrix() -assert nanoeigenpy.is_approx(A_reconstructed, A) - -adjoint = ldlt.adjoint() -assert adjoint is ldlt - -A_cond = np.eye(dim) -ldlt_cond = nanoeigenpy.LDLT(A_cond) -estimated_r_cond_num = ldlt_cond.rcond() -assert abs(estimated_r_cond_num - 1) <= 1e-9 - -ldlt_compute = ldlt.compute(A) - -LDLT = ldlt.matrixLDLT() -LDLT_lower_without_diag = np.tril(LDLT, k=-1) -L_lower_without_diag = np.tril(L, k=-1) -assert nanoeigenpy.is_approx(LDLT_lower_without_diag, L_lower_without_diag) - -A_upper_without_diag = np.triu(A, k=1) -LLT_upper_without_diag = np.triu(LDLT, k=1) -assert nanoeigenpy.is_approx(A_upper_without_diag, LLT_upper_without_diag) - -LDLT_diag = np.diagonal(LDLT) -assert nanoeigenpy.is_approx(LDLT_diag, D) - -sigma = 3 -w = np.ones(dim) -ldlt.rankUpdate(w, sigma) -L = ldlt.matrixL() -D = ldlt.vectorD() -P = ldlt.transpositionsP() -A_updated = np.transpose(P).dot(L.dot(np.diag(D).dot(np.transpose(L).dot(P)))) -assert nanoeigenpy.is_approx(A_updated, A + sigma * w * np.transpose(w)) - -ldlt1 = nanoeigenpy.LDLT() -ldlt2 = nanoeigenpy.LDLT() - -id1 = ldlt1.id() -id2 = ldlt2.id() - -assert id1 != id2 -assert id1 == ldlt1.id() -assert id2 == ldlt2.id() - -dim_constructor = 3 - -ldlt3 = nanoeigenpy.LDLT(dim_constructor) -ldlt4 = nanoeigenpy.LDLT(dim_constructor) - -id3 = ldlt3.id() -id4 = ldlt4.id() - -assert id3 != id4 -assert id3 == ldlt3.id() -assert id4 == ldlt4.id() + +def test_ldlt(): + dim = 100 + rng = np.random.default_rng() + + A_neg = -np.eye(dim) + ldlt_neg = nanoeigenpy.LDLT(A_neg) + assert ldlt_neg.isNegative() + assert not ldlt_neg.isPositive() + + A_pos = np.eye(dim) + ldlt_pos = nanoeigenpy.LDLT(A_pos) + assert ldlt_pos.isPositive() + assert not ldlt_pos.isNegative() + + A = rng.random((dim, dim)) + A = (A + A.T) * 0.5 + np.diag(10.0 + rng.random(dim)) + + ldlt = nanoeigenpy.LDLT(A) + assert ldlt.info() == nanoeigenpy.ComputationInfo.Success + + L = ldlt.matrixL() + D = ldlt.vectorD() + P = ldlt.transpositionsP() + assert nanoeigenpy.is_approx( + np.transpose(P).dot(L.dot(np.diag(D).dot(np.transpose(L).dot(P)))), A + ) + + X = rng.random((dim, 20)) + B = A.dot(X) + X_est = ldlt.solve(B) + assert nanoeigenpy.is_approx(X, X_est) + assert nanoeigenpy.is_approx(A.dot(X_est), B) + + x = rng.random(dim) + b = A.dot(x) + x_est = ldlt.solve(b) + assert nanoeigenpy.is_approx(x, x_est) + assert nanoeigenpy.is_approx(A.dot(x_est), b) + + A_reconstructed = ldlt.reconstructedMatrix() + assert nanoeigenpy.is_approx(A_reconstructed, A) + + adjoint = ldlt.adjoint() + assert adjoint is ldlt + + A_cond = np.eye(dim) + ldlt_cond = nanoeigenpy.LDLT(A_cond) + estimated_r_cond_num = ldlt_cond.rcond() + assert abs(estimated_r_cond_num - 1) <= 1e-9 + + ldlt_compute = ldlt.compute(A) # noqa + + LDLT = ldlt.matrixLDLT() + LDLT_lower_without_diag = np.tril(LDLT, k=-1) + L_lower_without_diag = np.tril(L, k=-1) + assert nanoeigenpy.is_approx(LDLT_lower_without_diag, L_lower_without_diag) + + A_upper_without_diag = np.triu(A, k=1) + LLT_upper_without_diag = np.triu(LDLT, k=1) + assert nanoeigenpy.is_approx(A_upper_without_diag, LLT_upper_without_diag) + + LDLT_diag = np.diagonal(LDLT) + assert nanoeigenpy.is_approx(LDLT_diag, D) + + sigma = 3 + w = np.ones(dim) + ldlt.rankUpdate(w, sigma) + L = ldlt.matrixL() + D = ldlt.vectorD() + P = ldlt.transpositionsP() + A_updated = np.transpose(P).dot(L.dot(np.diag(D).dot(np.transpose(L).dot(P)))) + assert nanoeigenpy.is_approx(A_updated, A + sigma * w * np.transpose(w)) + + ldlt1 = nanoeigenpy.LDLT() + ldlt2 = nanoeigenpy.LDLT() + + id1 = ldlt1.id() + id2 = ldlt2.id() + + assert id1 != id2 + assert id1 == ldlt1.id() + assert id2 == ldlt2.id() + + dim_constructor = 3 + + ldlt3 = nanoeigenpy.LDLT(dim_constructor) + ldlt4 = nanoeigenpy.LDLT(dim_constructor) + + id3 = ldlt3.id() + id4 = ldlt4.id() + + assert id3 != id4 + assert id3 == ldlt3.id() + assert id4 == ldlt4.id() diff --git a/tests/test_llt.py b/tests/test_llt.py index f2b953d..dd0e6c9 100644 --- a/tests/test_llt.py +++ b/tests/test_llt.py @@ -1,80 +1,82 @@ import nanoeigenpy import numpy as np -dim = 100 -rng = np.random.default_rng() -A = rng.random((dim, dim)) -A = (A + A.T) * 0.5 + np.diag(10.0 + rng.random(dim)) +def test_llt(): + dim = 100 + rng = np.random.default_rng() -llt = nanoeigenpy.LLT(A) + A = rng.random((dim, dim)) + A = (A + A.T) * 0.5 + np.diag(10.0 + rng.random(dim)) -assert llt.info() == nanoeigenpy.ComputationInfo.Success + llt = nanoeigenpy.LLT(A) -L = llt.matrixL() -assert nanoeigenpy.is_approx(L.dot(np.transpose(L)), A) + assert llt.info() == nanoeigenpy.ComputationInfo.Success -U = llt.matrixU() -LU = L @ U -assert nanoeigenpy.is_approx(LU, A) + L = llt.matrixL() + assert nanoeigenpy.is_approx(L.dot(np.transpose(L)), A) -X = rng.random((dim, 20)) -B = A.dot(X) -X_est = llt.solve(B) -assert nanoeigenpy.is_approx(X, X_est) -assert nanoeigenpy.is_approx(A.dot(X_est), B) + U = llt.matrixU() + LU = L @ U + assert nanoeigenpy.is_approx(LU, A) -x = rng.random(dim) -b = A.dot(x) -x_est = llt.solve(b) -assert nanoeigenpy.is_approx(x, x_est) -assert nanoeigenpy.is_approx(A.dot(x_est), b) + X = rng.random((dim, 20)) + B = A.dot(X) + X_est = llt.solve(B) + assert nanoeigenpy.is_approx(X, X_est) + assert nanoeigenpy.is_approx(A.dot(X_est), B) -LLT = llt.matrixLLT() -LLT_lower = np.tril(LLT) -assert nanoeigenpy.is_approx(LLT_lower, L) + x = rng.random(dim) + b = A.dot(x) + x_est = llt.solve(b) + assert nanoeigenpy.is_approx(x, x_est) + assert nanoeigenpy.is_approx(A.dot(x_est), b) -A_upper = np.triu(A, k=1) -LLT_upper = np.triu(LLT, k=1) -assert nanoeigenpy.is_approx(A_upper, LLT_upper) + LLT = llt.matrixLLT() + LLT_lower = np.tril(LLT) + assert nanoeigenpy.is_approx(LLT_lower, L) -A_reconstructed = llt.reconstructedMatrix() -assert nanoeigenpy.is_approx(A_reconstructed, A) + A_upper = np.triu(A, k=1) + LLT_upper = np.triu(LLT, k=1) + assert nanoeigenpy.is_approx(A_upper, LLT_upper) -adjoint = llt.adjoint() -assert adjoint is llt + A_reconstructed = llt.reconstructedMatrix() + assert nanoeigenpy.is_approx(A_reconstructed, A) -A_cond = np.eye(dim) -llt_cond = nanoeigenpy.LLT(A_cond) -estimated_r_cond_num = llt_cond.rcond() -assert abs(estimated_r_cond_num - 1) <= 1e-9 + adjoint = llt.adjoint() + assert adjoint is llt -sigma = 3 -w = np.ones(dim) -llt.rankUpdate(w, sigma) -L = llt.matrixL() -U = llt.matrixU() -LU = L @ U -assert nanoeigenpy.is_approx(LU, A + sigma * w * np.transpose(w)) + A_cond = np.eye(dim) + llt_cond = nanoeigenpy.LLT(A_cond) + estimated_r_cond_num = llt_cond.rcond() + assert abs(estimated_r_cond_num - 1) <= 1e-9 -llt1 = nanoeigenpy.LLT() -llt2 = nanoeigenpy.LLT() + sigma = 3 + w = np.ones(dim) + llt.rankUpdate(w, sigma) + L = llt.matrixL() + U = llt.matrixU() + LU = L @ U + assert nanoeigenpy.is_approx(LU, A + sigma * w * np.transpose(w)) -id1 = llt1.id() -id2 = llt2.id() + llt1 = nanoeigenpy.LLT() + llt2 = nanoeigenpy.LLT() -assert id1 != id2 -assert id1 == llt1.id() -assert id2 == llt2.id() + id1 = llt1.id() + id2 = llt2.id() -dim_constructor = 3 + assert id1 != id2 + assert id1 == llt1.id() + assert id2 == llt2.id() -llt3 = nanoeigenpy.LLT(dim_constructor) -llt4 = nanoeigenpy.LLT(dim_constructor) + dim_constructor = 3 -id3 = llt3.id() -id4 = llt4.id() + llt3 = nanoeigenpy.LLT(dim_constructor) + llt4 = nanoeigenpy.LLT(dim_constructor) -assert id3 != id4 -assert id3 == llt3.id() -assert id4 == llt4.id() + id3 = llt3.id() + id4 = llt4.id() + + assert id3 != id4 + assert id3 == llt3.id() + assert id4 == llt4.id() diff --git a/tests/test_partial_piv_lu.py b/tests/test_partial_piv_lu.py index 98554c6..1e6e5ee 100644 --- a/tests/test_partial_piv_lu.py +++ b/tests/test_partial_piv_lu.py @@ -1,75 +1,77 @@ import nanoeigenpy import numpy as np -dim = 100 -rng = np.random.default_rng() -A = rng.random((dim, dim)) -A = (A + A.T) * 0.5 + np.diag(10.0 + rng.random(dim)) -partialpivlu = nanoeigenpy.PartialPivLU(A) -X = rng.random((dim, 20)) -B = A.dot(X) -X_est = partialpivlu.solve(B) -assert nanoeigenpy.is_approx(X, X_est) -assert nanoeigenpy.is_approx(A.dot(X_est), B) +def test_partial_piv_lu(): + dim = 100 + rng = np.random.default_rng() + A = rng.random((dim, dim)) + A = (A + A.T) * 0.5 + np.diag(10.0 + rng.random(dim)) + partialpivlu = nanoeigenpy.PartialPivLU(A) -x = rng.random(dim) -b = A.dot(x) -x_est = partialpivlu.solve(b) -assert nanoeigenpy.is_approx(x, x_est) -assert nanoeigenpy.is_approx(A.dot(x_est), b) + X = rng.random((dim, 20)) + B = A.dot(X) + X_est = partialpivlu.solve(B) + assert nanoeigenpy.is_approx(X, X_est) + assert nanoeigenpy.is_approx(A.dot(X_est), B) -rows = partialpivlu.rows() -cols = partialpivlu.cols() -assert cols == dim -assert rows == dim + x = rng.random(dim) + b = A.dot(x) + x_est = partialpivlu.solve(b) + assert nanoeigenpy.is_approx(x, x_est) + assert nanoeigenpy.is_approx(A.dot(x_est), b) -partialpivlu_compute = partialpivlu.compute(A) -A_reconstructed = partialpivlu.reconstructedMatrix() -assert nanoeigenpy.is_approx(A_reconstructed, A) + rows = partialpivlu.rows() + cols = partialpivlu.cols() + assert cols == dim + assert rows == dim -LU = partialpivlu.matrixLU() -P_perm = partialpivlu.permutationP() -P = P_perm.toDenseMatrix() + partialpivlu_compute = partialpivlu.compute(A) # noqa + A_reconstructed = partialpivlu.reconstructedMatrix() + assert nanoeigenpy.is_approx(A_reconstructed, A) -U = np.triu(LU) -L = np.eye(dim) + np.tril(LU, -1) -assert nanoeigenpy.is_approx(P @ A, L @ U) + LU = partialpivlu.matrixLU() + P_perm = partialpivlu.permutationP() + P = P_perm.toDenseMatrix() -inverse = partialpivlu.inverse() -assert nanoeigenpy.is_approx(A @ inverse, np.eye(dim)) -assert nanoeigenpy.is_approx(inverse @ A, np.eye(dim)) + U = np.triu(LU) + L = np.eye(dim) + np.tril(LU, -1) + assert nanoeigenpy.is_approx(P @ A, L @ U) -rcond = partialpivlu.rcond() -determinant = partialpivlu.determinant() -det_numpy = np.linalg.det(A) -assert rcond > 0 -assert abs(determinant - det_numpy) / abs(det_numpy) < 1e-10 + inverse = partialpivlu.inverse() + assert nanoeigenpy.is_approx(A @ inverse, np.eye(dim)) + assert nanoeigenpy.is_approx(inverse @ A, np.eye(dim)) -P_inv = P_perm.inverse().toDenseMatrix() -assert nanoeigenpy.is_approx(P @ P_inv, np.eye(dim)) -assert nanoeigenpy.is_approx(P_inv @ P, np.eye(dim)) + rcond = partialpivlu.rcond() + determinant = partialpivlu.determinant() + det_numpy = np.linalg.det(A) + assert rcond > 0 + assert abs(determinant - det_numpy) / abs(det_numpy) < 1e-10 -decomp1 = nanoeigenpy.PartialPivLU() -decomp2 = nanoeigenpy.PartialPivLU() -id1 = decomp1.id() -id2 = decomp2.id() -assert id1 != id2 -assert id1 == decomp1.id() -assert id2 == decomp2.id() + P_inv = P_perm.inverse().toDenseMatrix() + assert nanoeigenpy.is_approx(P @ P_inv, np.eye(dim)) + assert nanoeigenpy.is_approx(P_inv @ P, np.eye(dim)) -decomp3 = nanoeigenpy.PartialPivLU(dim) -decomp4 = nanoeigenpy.PartialPivLU(dim) -id3 = decomp3.id() -id4 = decomp4.id() -assert id3 != id4 -assert id3 == decomp3.id() -assert id4 == decomp4.id() + decomp1 = nanoeigenpy.PartialPivLU() + decomp2 = nanoeigenpy.PartialPivLU() + id1 = decomp1.id() + id2 = decomp2.id() + assert id1 != id2 + assert id1 == decomp1.id() + assert id2 == decomp2.id() -decomp5 = nanoeigenpy.PartialPivLU(A) -decomp6 = nanoeigenpy.PartialPivLU(A) -id5 = decomp5.id() -id6 = decomp6.id() -assert id5 != id6 -assert id5 == decomp5.id() -assert id6 == decomp6.id() + decomp3 = nanoeigenpy.PartialPivLU(dim) + decomp4 = nanoeigenpy.PartialPivLU(dim) + id3 = decomp3.id() + id4 = decomp4.id() + assert id3 != id4 + assert id3 == decomp3.id() + assert id4 == decomp4.id() + + decomp5 = nanoeigenpy.PartialPivLU(A) + decomp6 = nanoeigenpy.PartialPivLU(A) + id5 = decomp5.id() + id6 = decomp6.id() + assert id5 != id6 + assert id5 == decomp5.id() + assert id6 == decomp6.id() diff --git a/tests/test_permutation_matrix.py b/tests/test_permutation_matrix.py index 29e76f8..95c74f6 100644 --- a/tests/test_permutation_matrix.py +++ b/tests/test_permutation_matrix.py @@ -1,59 +1,61 @@ import nanoeigenpy import numpy as np -dim = 100 -rng = np.random.default_rng() -indices = rng.permutation(dim) -perm = nanoeigenpy.PermutationMatrix(dim) -perm = nanoeigenpy.PermutationMatrix(indices) +def test_permutation_matrix(): + dim = 100 + rng = np.random.default_rng() + indices = rng.permutation(dim) -est_indices = perm.indices() -assert est_indices.all() == indices.all() + perm = nanoeigenpy.PermutationMatrix(dim) + perm = nanoeigenpy.PermutationMatrix(indices) -perm_left = perm.applyTranspositionOnTheLeft(0, 1) -perm_left_right = perm_left.applyTranspositionOnTheRight(0, 1) -assert perm_left_right.indices().all() == perm.indices().all() + est_indices = perm.indices() + assert est_indices.all() == indices.all() -perm.setIdentity() -assert perm.indices().all() == np.arange(dim).all() -dim = dim + 1 -perm.setIdentity(dim) -assert perm.indices().all() == np.arange(dim).all() + perm_left = perm.applyTranspositionOnTheLeft(0, 1) + perm_left_right = perm_left.applyTranspositionOnTheRight(0, 1) + assert perm_left_right.indices().all() == perm.indices().all() -perm.setIdentity() -dense = perm.toDenseMatrix() -assert dense.all() == np.eye(dim).all() + perm.setIdentity() + assert perm.indices().all() == np.arange(dim).all() + dim = dim + 1 + perm.setIdentity(dim) + assert perm.indices().all() == np.arange(dim).all() -perm = nanoeigenpy.PermutationMatrix(np.array([1, 0, 2])) -perm_t = perm.transpose() -dense = perm.toDenseMatrix() -dense_t = perm_t.toDenseMatrix() -assert dense_t.all() == dense.T.all() + perm.setIdentity() + dense = perm.toDenseMatrix() + assert dense.all() == np.eye(dim).all() -perm_inv = perm.inverse() -result = perm * perm_inv -identity = result.toDenseMatrix() -assert identity.all() == np.eye(3).all() + perm = nanoeigenpy.PermutationMatrix(np.array([1, 0, 2])) + perm_t = perm.transpose() + dense = perm.toDenseMatrix() + dense_t = perm_t.toDenseMatrix() + assert dense_t.all() == dense.T.all() -dim_constructor = 3 + perm_inv = perm.inverse() + result = perm * perm_inv + identity = result.toDenseMatrix() + assert identity.all() == np.eye(3).all() -perm1 = nanoeigenpy.PermutationMatrix(dim_constructor) -perm2 = nanoeigenpy.PermutationMatrix(dim_constructor) + dim_constructor = 3 -id1 = perm1.id() -id2 = perm2.id() + perm1 = nanoeigenpy.PermutationMatrix(dim_constructor) + perm2 = nanoeigenpy.PermutationMatrix(dim_constructor) -assert id1 != id2 -assert id1 == perm1.id() -assert id2 == perm2.id() + id1 = perm1.id() + id2 = perm2.id() -es3 = nanoeigenpy.PermutationMatrix(indices) -es4 = nanoeigenpy.PermutationMatrix(indices) + assert id1 != id2 + assert id1 == perm1.id() + assert id2 == perm2.id() -id3 = es3.id() -id4 = es4.id() + es3 = nanoeigenpy.PermutationMatrix(indices) + es4 = nanoeigenpy.PermutationMatrix(indices) -assert id3 != id4 -assert id3 == es3.id() -assert id4 == es4.id() + id3 = es3.id() + id4 = es4.id() + + assert id3 != id4 + assert id3 == es3.id() + assert id4 == es4.id() diff --git a/tests/test_qr.py b/tests/test_qr.py index fa0f194..5dacbbd 100644 --- a/tests/test_qr.py +++ b/tests/test_qr.py @@ -1,100 +1,102 @@ import nanoeigenpy import numpy as np -rows = 20 -cols = 100 -rng = np.random.default_rng() - -A = rng.random((rows, cols)) - -householder_qr = nanoeigenpy.HouseholderQR() -householder_qr = nanoeigenpy.HouseholderQR(rows, cols) -householder_qr = nanoeigenpy.HouseholderQR(A) - -householder_qr_eye = nanoeigenpy.HouseholderQR(np.eye(rows, rows)) -X = rng.random((rows, 20)) -assert householder_qr_eye.absDeterminant() == 1.0 -assert householder_qr_eye.logAbsDeterminant() == 0.0 - -Y = householder_qr_eye.solve(X) -assert (X == Y).all() - -x = rng.random(rows) -y = householder_qr_eye.solve(x) -assert (x == y).all() - -fullpiv_householder_qr = nanoeigenpy.FullPivHouseholderQR() -fullpiv_householder_qr = nanoeigenpy.FullPivHouseholderQR(rows, cols) -fullpiv_householder_qr = nanoeigenpy.FullPivHouseholderQR(A) - -fullpiv_householder_qr = nanoeigenpy.FullPivHouseholderQR(np.eye(rows, rows)) -assert fullpiv_householder_qr.isSurjective() -assert fullpiv_householder_qr.isInjective() -fullpiv_householder_qr.isInvertible() - -X = rng.random((rows, 20)) -assert fullpiv_householder_qr.absDeterminant() == 1.0 -assert fullpiv_householder_qr.logAbsDeterminant() == 0.0 - -Y = fullpiv_householder_qr.solve(X) -assert (X == Y).all() -assert fullpiv_householder_qr.rank() == rows - -x = rng.random(rows) -y = fullpiv_householder_qr.solve(x) -assert (x == y).all() - -fullpiv_householder_qr.setThreshold() -fullpiv_householder_qr.setThreshold(1e-8) -assert fullpiv_householder_qr.threshold() == 1e-8 -assert nanoeigenpy.is_approx(np.eye(rows, rows), fullpiv_householder_qr.inverse()) - -assert fullpiv_householder_qr.maxPivot() == 1.0 -assert fullpiv_householder_qr.nonzeroPivots() == rows -assert fullpiv_householder_qr.dimensionOfKernel() == 0 - -colpiv_householder_qr = nanoeigenpy.ColPivHouseholderQR(A) -assert colpiv_householder_qr.info() == nanoeigenpy.ComputationInfo.Success - -colpiv_householder_qr = nanoeigenpy.ColPivHouseholderQR(np.eye(rows, rows)) -X = rng.random((rows, 20)) -assert colpiv_householder_qr.absDeterminant() == 1.0 -assert colpiv_householder_qr.logAbsDeterminant() == 0.0 - -Y = colpiv_householder_qr.solve(X) -assert (X == Y).all() -assert colpiv_householder_qr.rank() == rows - -colpiv_householder_qr.setThreshold() -colpiv_householder_qr.setThreshold(1e-8) -assert colpiv_householder_qr.threshold() == 1e-8 -assert nanoeigenpy.is_approx(np.eye(rows, rows), colpiv_householder_qr.inverse()) - -assert colpiv_householder_qr.maxPivot() == 1.0 -assert colpiv_householder_qr.nonzeroPivots() == rows -assert colpiv_householder_qr.dimensionOfKernel() == 0 - -cod = nanoeigenpy.CompleteOrthogonalDecomposition(A) -assert cod.info() == nanoeigenpy.ComputationInfo.Success - -cod = nanoeigenpy.CompleteOrthogonalDecomposition(np.eye(rows, rows)) -X = rng.random((rows, 20)) -assert cod.absDeterminant() == 1.0 -assert cod.logAbsDeterminant() == 0.0 - -Y = cod.solve(X) -assert (X == Y).all() -assert cod.rank() == rows - -x = rng.random(rows) -y = cod.solve(x) -assert (x == y).all() - -cod.setThreshold() -cod.setThreshold(1e-8) -assert cod.threshold() == 1e-8 -assert nanoeigenpy.is_approx(np.eye(rows, rows), cod.pseudoInverse()) - -assert cod.maxPivot() == 1.0 -assert cod.nonzeroPivots() == rows -assert cod.dimensionOfKernel() == 0 + +def test_qr(): + rows = 20 + cols = 100 + rng = np.random.default_rng() + + A = rng.random((rows, cols)) + + householder_qr = nanoeigenpy.HouseholderQR() + householder_qr = nanoeigenpy.HouseholderQR(rows, cols) + householder_qr = nanoeigenpy.HouseholderQR(A) # noqa + + householder_qr_eye = nanoeigenpy.HouseholderQR(np.eye(rows, rows)) + X = rng.random((rows, 20)) + assert householder_qr_eye.absDeterminant() == 1.0 + assert householder_qr_eye.logAbsDeterminant() == 0.0 + + Y = householder_qr_eye.solve(X) + assert (X == Y).all() + + x = rng.random(rows) + y = householder_qr_eye.solve(x) + assert (x == y).all() + + fullpiv_householder_qr = nanoeigenpy.FullPivHouseholderQR() + fullpiv_householder_qr = nanoeigenpy.FullPivHouseholderQR(rows, cols) + fullpiv_householder_qr = nanoeigenpy.FullPivHouseholderQR(A) + + fullpiv_householder_qr = nanoeigenpy.FullPivHouseholderQR(np.eye(rows, rows)) + assert fullpiv_householder_qr.isSurjective() + assert fullpiv_householder_qr.isInjective() + fullpiv_householder_qr.isInvertible() + + X = rng.random((rows, 20)) + assert fullpiv_householder_qr.absDeterminant() == 1.0 + assert fullpiv_householder_qr.logAbsDeterminant() == 0.0 + + Y = fullpiv_householder_qr.solve(X) + assert (X == Y).all() + assert fullpiv_householder_qr.rank() == rows + + x = rng.random(rows) + y = fullpiv_householder_qr.solve(x) + assert (x == y).all() + + fullpiv_householder_qr.setThreshold() + fullpiv_householder_qr.setThreshold(1e-8) + assert fullpiv_householder_qr.threshold() == 1e-8 + assert nanoeigenpy.is_approx(np.eye(rows, rows), fullpiv_householder_qr.inverse()) + + assert fullpiv_householder_qr.maxPivot() == 1.0 + assert fullpiv_householder_qr.nonzeroPivots() == rows + assert fullpiv_householder_qr.dimensionOfKernel() == 0 + + colpiv_householder_qr = nanoeigenpy.ColPivHouseholderQR(A) + assert colpiv_householder_qr.info() == nanoeigenpy.ComputationInfo.Success + + colpiv_householder_qr = nanoeigenpy.ColPivHouseholderQR(np.eye(rows, rows)) + X = rng.random((rows, 20)) + assert colpiv_householder_qr.absDeterminant() == 1.0 + assert colpiv_householder_qr.logAbsDeterminant() == 0.0 + + Y = colpiv_householder_qr.solve(X) + assert (X == Y).all() + assert colpiv_householder_qr.rank() == rows + + colpiv_householder_qr.setThreshold() + colpiv_householder_qr.setThreshold(1e-8) + assert colpiv_householder_qr.threshold() == 1e-8 + assert nanoeigenpy.is_approx(np.eye(rows, rows), colpiv_householder_qr.inverse()) + + assert colpiv_householder_qr.maxPivot() == 1.0 + assert colpiv_householder_qr.nonzeroPivots() == rows + assert colpiv_householder_qr.dimensionOfKernel() == 0 + + cod = nanoeigenpy.CompleteOrthogonalDecomposition(A) + assert cod.info() == nanoeigenpy.ComputationInfo.Success + + cod = nanoeigenpy.CompleteOrthogonalDecomposition(np.eye(rows, rows)) + X = rng.random((rows, 20)) + assert cod.absDeterminant() == 1.0 + assert cod.logAbsDeterminant() == 0.0 + + Y = cod.solve(X) + assert (X == Y).all() + assert cod.rank() == rows + + x = rng.random(rows) + y = cod.solve(x) + assert (x == y).all() + + cod.setThreshold() + cod.setThreshold(1e-8) + assert cod.threshold() == 1e-8 + assert nanoeigenpy.is_approx(np.eye(rows, rows), cod.pseudoInverse()) + + assert cod.maxPivot() == 1.0 + assert cod.nonzeroPivots() == rows + assert cod.dimensionOfKernel() == 0 diff --git a/tests/test_real_qz.py b/tests/test_real_qz.py index 088708d..e778f05 100644 --- a/tests/test_real_qz.py +++ b/tests/test_real_qz.py @@ -1,37 +1,39 @@ import nanoeigenpy import numpy as np -dim = 100 -rng = np.random.default_rng() -A = rng.random((dim, dim)) -B = rng.random((dim, dim)) - -realqz = nanoeigenpy.RealQZ(A, B) -assert realqz.info() == nanoeigenpy.ComputationInfo.Success - -Q = realqz.matrixQ() -S = realqz.matrixS() -Z = realqz.matrixZ() -T = realqz.matrixT() - -assert nanoeigenpy.is_approx(A, Q @ S @ Z) -assert nanoeigenpy.is_approx(B, Q @ T @ Z) - -assert nanoeigenpy.is_approx(Q @ Q.T, np.eye(dim)) -assert nanoeigenpy.is_approx(Z @ Z.T, np.eye(dim)) - -for i in range(dim): - for j in range(i): - assert abs(T[i, j]) < 1e-12 - -for i in range(dim): - for j in range(i - 1): - assert abs(S[i, j]) < 1e-12 - -realqz3_id = nanoeigenpy.RealQZ(A, B) -realqz4_id = nanoeigenpy.RealQZ(A, B) -id3 = realqz3_id.id() -id4 = realqz4_id.id() -assert id3 != id4 -assert id3 == realqz3_id.id() -assert id4 == realqz4_id.id() + +def test_real_qz(): + dim = 100 + rng = np.random.default_rng() + A = rng.random((dim, dim)) + B = rng.random((dim, dim)) + + realqz = nanoeigenpy.RealQZ(A, B) + assert realqz.info() == nanoeigenpy.ComputationInfo.Success + + Q = realqz.matrixQ() + S = realqz.matrixS() + Z = realqz.matrixZ() + T = realqz.matrixT() + + assert nanoeigenpy.is_approx(A, Q @ S @ Z) + assert nanoeigenpy.is_approx(B, Q @ T @ Z) + + assert nanoeigenpy.is_approx(Q @ Q.T, np.eye(dim)) + assert nanoeigenpy.is_approx(Z @ Z.T, np.eye(dim)) + + for i in range(dim): + for j in range(i): + assert abs(T[i, j]) < 1e-12 + + for i in range(dim): + for j in range(i - 1): + assert abs(S[i, j]) < 1e-12 + + realqz3_id = nanoeigenpy.RealQZ(A, B) + realqz4_id = nanoeigenpy.RealQZ(A, B) + id3 = realqz3_id.id() + id4 = realqz4_id.id() + assert id3 != id4 + assert id3 == realqz3_id.id() + assert id4 == realqz4_id.id() diff --git a/tests/test_real_schur.py b/tests/test_real_schur.py index 399675a..9e9efe7 100644 --- a/tests/test_real_schur.py +++ b/tests/test_real_schur.py @@ -2,47 +2,50 @@ import numpy as np -def verify_is_quasi_triangular(T): - size = T.shape[0] +def test_real_schur(): + def verify_is_quasi_triangular(T): + size = T.shape[0] - for row in range(2, size): - for col in range(row - 1): - assert abs(T[row, col]) < 1e-12 + for row in range(2, size): + for col in range(row - 1): + assert abs(T[row, col]) < 1e-12 - for row in range(1, size): - if abs(T[row, row - 1]) > 1e-12: - if row < size - 1: - assert abs(T[row + 1, row]) < 1e-12 + for row in range(1, size): + if abs(T[row, row - 1]) > 1e-12: + if row < size - 1: + assert abs(T[row + 1, row]) < 1e-12 - tr = T[row - 1, row - 1] + T[row, row] - det = T[row - 1, row - 1] * T[row, row] - T[row - 1, row] * T[row, row - 1] - assert 4 * det > tr * tr + tr = T[row - 1, row - 1] + T[row, row] + det = ( + T[row - 1, row - 1] * T[row, row] + - T[row - 1, row] * T[row, row - 1] + ) + assert 4 * det > tr * tr + dim = 100 + rng = np.random.default_rng() + A = rng.random((dim, dim)) -dim = 100 -rng = np.random.default_rng() -A = rng.random((dim, dim)) + rs = nanoeigenpy.RealSchur(A) + assert rs.info() == nanoeigenpy.ComputationInfo.Success -rs = nanoeigenpy.RealSchur(A) -assert rs.info() == nanoeigenpy.ComputationInfo.Success + U = rs.matrixU() + T = rs.matrixT() -U = rs.matrixU() -T = rs.matrixT() + assert nanoeigenpy.is_approx(A, U @ T @ U.T) + assert nanoeigenpy.is_approx(U @ U.T, np.eye(dim)) -assert nanoeigenpy.is_approx(A, U @ T @ U.T) -assert nanoeigenpy.is_approx(U @ U.T, np.eye(dim)) + verify_is_quasi_triangular(T) -verify_is_quasi_triangular(T) + hess = nanoeigenpy.HessenbergDecomposition(A) + H = hess.matrixH() + Q_hess = hess.matrixQ() -hess = nanoeigenpy.HessenbergDecomposition(A) -H = hess.matrixH() -Q_hess = hess.matrixQ() + rs_from_hess = nanoeigenpy.RealSchur(dim) + result_from_hess = rs_from_hess.computeFromHessenberg(H, Q_hess, True) + assert result_from_hess.info() == nanoeigenpy.ComputationInfo.Success -rs_from_hess = nanoeigenpy.RealSchur(dim) -result_from_hess = rs_from_hess.computeFromHessenberg(H, Q_hess, True) -assert result_from_hess.info() == nanoeigenpy.ComputationInfo.Success + T_from_hess = rs_from_hess.matrixT() + U_from_hess = rs_from_hess.matrixU() -T_from_hess = rs_from_hess.matrixT() -U_from_hess = rs_from_hess.matrixU() - -assert nanoeigenpy.is_approx(A, U_from_hess @ T_from_hess @ U_from_hess.T) + assert nanoeigenpy.is_approx(A, U_from_hess @ T_from_hess @ U_from_hess.T) diff --git a/tests/test_self_adjoint_eigen_solver.py b/tests/test_self_adjoint_eigen_solver.py index d53ed89..ae03b73 100644 --- a/tests/test_self_adjoint_eigen_solver.py +++ b/tests/test_self_adjoint_eigen_solver.py @@ -1,20 +1,22 @@ import nanoeigenpy import numpy as np -dim = 100 -rng = np.random.default_rng() -A = rng.random((dim, dim)) -A = (A + A.T) * 0.5 +def test_self_adjoint_eigen_solver(): + dim = 100 + rng = np.random.default_rng() -es = nanoeigenpy.SelfAdjointEigenSolver(A) + A = rng.random((dim, dim)) + A = (A + A.T) * 0.5 -assert es.info() == nanoeigenpy.ComputationInfo.Success + es = nanoeigenpy.SelfAdjointEigenSolver(A) -V = es.eigenvectors() -D = es.eigenvalues() + assert es.info() == nanoeigenpy.ComputationInfo.Success -AdotV = A @ V -VdotD = V @ np.diag(D) + V = es.eigenvectors() + D = es.eigenvalues() -assert nanoeigenpy.is_approx(AdotV, VdotD, 1e-6) + AdotV = A @ V + VdotD = V @ np.diag(D) + + assert nanoeigenpy.is_approx(AdotV, VdotD, 1e-6) diff --git a/tests/test_simplicial_llt.py b/tests/test_simplicial_llt.py index 12e8d46..d613fb7 100644 --- a/tests/test_simplicial_llt.py +++ b/tests/test_simplicial_llt.py @@ -2,45 +2,47 @@ import numpy as np import scipy.sparse as spa -dim = 100 -rng = np.random.default_rng() - -A_fac = spa.random(dim, dim, density=0.25, random_state=rng) -A = A_fac.T @ A_fac -A += spa.diags(10.0 * rng.standard_normal(dim) ** 2) -A = A.tocsc(True) -A.check_format() - -llt = nanoeigenpy.SimplicialLLT(A) - -assert llt.info() == nanoeigenpy.ComputationInfo.Success - -L = llt.matrixL() -U = llt.matrixU() - -LU = L @ U -perm = llt.permutationP().toDenseMatrix() -perm_inv = llt.permutationP().inverse().toDenseMatrix() -A_perm = perm @ A @ perm_inv -assert nanoeigenpy.is_approx(LU.toarray(), A_perm) - -X = rng.random((dim, 20)) -B = A.dot(X) -X_est = llt.solve(B) -assert isinstance(X_est, np.ndarray) -assert nanoeigenpy.is_approx(X, X_est) -assert nanoeigenpy.is_approx(A.dot(X_est), B) - -llt.analyzePattern(A) -llt.factorize(A) - -X_sparse = spa.random(dim, 10, random_state=rng) -B_sparse = A.dot(X_sparse) -B_sparse: spa.csc_matrix = B_sparse.tocsc(True) -if not B_sparse.has_sorted_indices: - B_sparse.sort_indices() - -X_est = llt.solve(B_sparse) -assert isinstance(X_est, spa.csc_matrix) -assert nanoeigenpy.is_approx(X_est.toarray(), X_sparse.toarray()) -assert nanoeigenpy.is_approx(A.dot(X_est.toarray()), B_sparse.toarray()) + +def test_simplicial_llt(): + dim = 100 + rng = np.random.default_rng() + + A_fac = spa.random(dim, dim, density=0.25, random_state=rng) + A = A_fac.T @ A_fac + A += spa.diags(10.0 * rng.standard_normal(dim) ** 2) + A = A.tocsc(True) + A.check_format() + + llt = nanoeigenpy.SimplicialLLT(A) + + assert llt.info() == nanoeigenpy.ComputationInfo.Success + + L = llt.matrixL() + U = llt.matrixU() + + LU = L @ U + perm = llt.permutationP().toDenseMatrix() + perm_inv = llt.permutationP().inverse().toDenseMatrix() + A_perm = perm @ A @ perm_inv + assert nanoeigenpy.is_approx(LU.toarray(), A_perm) + + X = rng.random((dim, 20)) + B = A.dot(X) + X_est = llt.solve(B) + assert isinstance(X_est, np.ndarray) + assert nanoeigenpy.is_approx(X, X_est) + assert nanoeigenpy.is_approx(A.dot(X_est), B) + + llt.analyzePattern(A) + llt.factorize(A) + + X_sparse = spa.random(dim, 10, random_state=rng) + B_sparse = A.dot(X_sparse) + B_sparse: spa.csc_matrix = B_sparse.tocsc(True) + if not B_sparse.has_sorted_indices: + B_sparse.sort_indices() + + X_est = llt.solve(B_sparse) + assert isinstance(X_est, spa.csc_matrix) + assert nanoeigenpy.is_approx(X_est.toarray(), X_sparse.toarray()) + assert nanoeigenpy.is_approx(A.dot(X_est.toarray()), B_sparse.toarray()) diff --git a/tests/test_sparse_lu.py b/tests/test_sparse_lu.py index 859b1f7..5c5fa72 100644 --- a/tests/test_sparse_lu.py +++ b/tests/test_sparse_lu.py @@ -2,62 +2,64 @@ import numpy as np import scipy.sparse as spa -dim = 100 -rng = np.random.default_rng() - -A_fac = spa.random(dim, dim, density=0.25, random_state=rng) -A = A_fac.T @ A_fac -A += spa.diags(10.0 * rng.standard_normal(dim) ** 2) -A = A.tocsc(True) -A.check_format() - -splu = nanoeigenpy.SparseLU(A) - -assert splu.info() == nanoeigenpy.ComputationInfo.Success - -X = rng.random((dim, 20)) -B = A.dot(X) -X_est = splu.solve(B) -assert isinstance(X_est, np.ndarray) -assert nanoeigenpy.is_approx(X, X_est) -assert nanoeigenpy.is_approx(A.dot(X_est), B) - -splu.analyzePattern(A) -splu.factorize(A) - -X_sparse = spa.random(dim, 10, random_state=rng) -B_sparse = A.dot(X_sparse) -B_sparse: spa.csc_matrix = B_sparse.tocsc(True) -if not B_sparse.has_sorted_indices: - B_sparse.sort_indices() - -X_est = splu.solve(B_sparse) -assert isinstance(X_est, spa.csc_matrix) -assert nanoeigenpy.is_approx(X_est.toarray(), X_sparse.toarray()) -assert nanoeigenpy.is_approx(A.dot(X_est.toarray()), B_sparse.toarray()) - -assert splu.nnzL() > 0 -assert splu.nnzU() > 0 - -L = splu.matrixL() -U = splu.matrixU() - -assert L.rows() == dim -assert L.cols() == dim -assert U.rows() == dim -assert U.cols() == dim - -x_true = rng.random(dim) -b_true = A.dot(x_true) -P_rows_indices = splu.rowsPermutation().indices() -P_cols_indices = splu.colsPermutation().indices() - -b_permuted = b_true[P_rows_indices] -z = b_permuted.copy() -L.solveInPlace(z) -y = z.copy() -U.solveInPlace(y) -x_reconstructed = np.zeros(dim) -x_reconstructed[P_cols_indices] = y - -assert nanoeigenpy.is_approx(x_reconstructed, x_true, 1e-6) + +def test_sparse_lu(): + dim = 100 + rng = np.random.default_rng() + + A_fac = spa.random(dim, dim, density=0.25, random_state=rng) + A = A_fac.T @ A_fac + A += spa.diags(10.0 * rng.standard_normal(dim) ** 2) + A = A.tocsc(True) + A.check_format() + + splu = nanoeigenpy.SparseLU(A) + + assert splu.info() == nanoeigenpy.ComputationInfo.Success + + X = rng.random((dim, 20)) + B = A.dot(X) + X_est = splu.solve(B) + assert isinstance(X_est, np.ndarray) + assert nanoeigenpy.is_approx(X, X_est) + assert nanoeigenpy.is_approx(A.dot(X_est), B) + + splu.analyzePattern(A) + splu.factorize(A) + + X_sparse = spa.random(dim, 10, random_state=rng) + B_sparse = A.dot(X_sparse) + B_sparse: spa.csc_matrix = B_sparse.tocsc(True) + if not B_sparse.has_sorted_indices: + B_sparse.sort_indices() + + X_est = splu.solve(B_sparse) + assert isinstance(X_est, spa.csc_matrix) + assert nanoeigenpy.is_approx(X_est.toarray(), X_sparse.toarray()) + assert nanoeigenpy.is_approx(A.dot(X_est.toarray()), B_sparse.toarray()) + + assert splu.nnzL() > 0 + assert splu.nnzU() > 0 + + L = splu.matrixL() + U = splu.matrixU() + + assert L.rows() == dim + assert L.cols() == dim + assert U.rows() == dim + assert U.cols() == dim + + x_true = rng.random(dim) + b_true = A.dot(x_true) + P_rows_indices = splu.rowsPermutation().indices() + P_cols_indices = splu.colsPermutation().indices() + + b_permuted = b_true[P_rows_indices] + z = b_permuted.copy() + L.solveInPlace(z) + y = z.copy() + U.solveInPlace(y) + x_reconstructed = np.zeros(dim) + x_reconstructed[P_cols_indices] = y + + assert nanoeigenpy.is_approx(x_reconstructed, x_true, 1e-6) diff --git a/tests/test_sparse_qr.py b/tests/test_sparse_qr.py index 7a825f8..9544cf0 100644 --- a/tests/test_sparse_qr.py +++ b/tests/test_sparse_qr.py @@ -2,72 +2,74 @@ import numpy as np import scipy.sparse as spa -dim = 100 -rng = np.random.default_rng() - -A_fac = spa.random(dim, dim, density=0.25, random_state=rng) -A = A_fac.T @ A_fac -A += spa.diags(10.0 * rng.standard_normal(dim) ** 2) -A = A.tocsc(True) -A.check_format() - -spqr = nanoeigenpy.SparseQR(A) - -assert spqr.info() == nanoeigenpy.ComputationInfo.Success - -X = rng.random((dim, 20)) -B = A.dot(X) -X_est = spqr.solve(B) -assert isinstance(X_est, np.ndarray) -assert nanoeigenpy.is_approx(X, X_est) -assert nanoeigenpy.is_approx(A.dot(X_est), B) - -spqr.analyzePattern(A) -spqr.factorize(A) - -X_sparse = spa.random(dim, 10, random_state=rng) -B_sparse = A.dot(X_sparse) -B_sparse: spa.csc_matrix = B_sparse.tocsc(True) -if not B_sparse.has_sorted_indices: - B_sparse.sort_indices() - -X_est = spqr.solve(B_sparse) -assert isinstance(X_est, spa.csc_matrix) -assert nanoeigenpy.is_approx(X_est.toarray(), X_sparse.toarray()) -assert nanoeigenpy.is_approx(A.dot(X_est.toarray()), B_sparse.toarray()) - -Q = spqr.matrixQ() -R = spqr.matrixR() -P = spqr.colsPermutation() - -assert spqr.matrixQ().rows() == dim -assert spqr.matrixQ().cols() == dim -assert R.shape[0] == dim -assert R.shape[1] == dim -assert P.indices().size == dim - -test_vec = rng.random(dim) -test_matrix = rng.random((dim, 20)) - -Qv = Q @ test_vec -QM = Q @ test_matrix -Qt = Q.transpose() -QtV = Qt @ test_vec -QtM = Qt @ test_matrix - -assert Qv.shape == (dim,) -assert QM.shape == (dim, 20) -assert QtV.shape == (dim,) -assert QtM.shape == (dim, 20) - -Qa_real_mat = Q.adjoint() -QaV = Qa_real_mat @ test_vec -assert nanoeigenpy.is_approx(QtV, QaV) - -A_dense = A.toarray() -P_indices = np.array([P.indices()[i] for i in range(dim)]) -A_permuted = A_dense[:, P_indices] - -QtAP = Qt @ A_permuted -R_dense = spqr.matrixR().toarray() -assert nanoeigenpy.is_approx(QtAP, R_dense) + +def test_sparse_qr(): + dim = 100 + rng = np.random.default_rng() + + A_fac = spa.random(dim, dim, density=0.25, random_state=rng) + A = A_fac.T @ A_fac + A += spa.diags(10.0 * rng.standard_normal(dim) ** 2) + A = A.tocsc(True) + A.check_format() + + spqr = nanoeigenpy.SparseQR(A) + + assert spqr.info() == nanoeigenpy.ComputationInfo.Success + + X = rng.random((dim, 20)) + B = A.dot(X) + X_est = spqr.solve(B) + assert isinstance(X_est, np.ndarray) + assert nanoeigenpy.is_approx(X, X_est) + assert nanoeigenpy.is_approx(A.dot(X_est), B) + + spqr.analyzePattern(A) + spqr.factorize(A) + + X_sparse = spa.random(dim, 10, random_state=rng) + B_sparse = A.dot(X_sparse) + B_sparse: spa.csc_matrix = B_sparse.tocsc(True) + if not B_sparse.has_sorted_indices: + B_sparse.sort_indices() + + X_est = spqr.solve(B_sparse) + assert isinstance(X_est, spa.csc_matrix) + assert nanoeigenpy.is_approx(X_est.toarray(), X_sparse.toarray()) + assert nanoeigenpy.is_approx(A.dot(X_est.toarray()), B_sparse.toarray()) + + Q = spqr.matrixQ() + R = spqr.matrixR() + P = spqr.colsPermutation() + + assert spqr.matrixQ().rows() == dim + assert spqr.matrixQ().cols() == dim + assert R.shape[0] == dim + assert R.shape[1] == dim + assert P.indices().size == dim + + test_vec = rng.random(dim) + test_matrix = rng.random((dim, 20)) + + Qv = Q @ test_vec + QM = Q @ test_matrix + Qt = Q.transpose() + QtV = Qt @ test_vec + QtM = Qt @ test_matrix + + assert Qv.shape == (dim,) + assert QM.shape == (dim, 20) + assert QtV.shape == (dim,) + assert QtM.shape == (dim, 20) + + Qa_real_mat = Q.adjoint() + QaV = Qa_real_mat @ test_vec + assert nanoeigenpy.is_approx(QtV, QaV) + + A_dense = A.toarray() + P_indices = np.array([P.indices()[i] for i in range(dim)]) + A_permuted = A_dense[:, P_indices] + + QtAP = Qt @ A_permuted + R_dense = spqr.matrixR().toarray() + assert nanoeigenpy.is_approx(QtAP, R_dense) diff --git a/tests/test_tridiagonalization.py b/tests/test_tridiagonalization.py index 7b8a2f7..9a6d8b5 100644 --- a/tests/test_tridiagonalization.py +++ b/tests/test_tridiagonalization.py @@ -1,103 +1,104 @@ import nanoeigenpy import numpy as np -dim = 100 -rng = np.random.default_rng() -A = rng.random((dim, dim)) -A = (A + A.T) * 0.5 - -tri = nanoeigenpy.Tridiagonalization(A) - -Q = tri.matrixQ() -T = tri.matrixT() - -assert nanoeigenpy.is_approx(A, Q @ T @ Q.T) -assert nanoeigenpy.is_approx(Q @ Q.T, np.eye(dim)) - -for i in range(dim): - for j in range(dim): - if abs(i - j) > 1: - assert abs(T[i, j]) < 1e-12 - -assert nanoeigenpy.is_approx(T, T.T) - -diag = tri.diagonal() -sub_diag = tri.subDiagonal() - -for i in range(dim): - assert abs(diag[i] - T[i, i]) < 1e-12 - -for i in range(dim - 1): - assert abs(sub_diag[i] - T[i + 1, i]) < 1e-12 - -A_test = rng.random((dim, dim)) -A_test = (A_test + A_test.T) * 0.5 - -tri1 = nanoeigenpy.Tridiagonalization(dim) -tri1.compute(A_test) -tri2 = nanoeigenpy.Tridiagonalization(A_test) - -Q1 = tri1.matrixQ() -T1 = tri1.matrixT() -Q2 = tri2.matrixQ() -T2 = tri2.matrixT() - -assert nanoeigenpy.is_approx(Q1, Q2) -assert nanoeigenpy.is_approx(T1, T2) - -h_coeffs = tri.householderCoefficients() -packed = tri.packedMatrix() - -assert h_coeffs.shape == (dim - 1,) -assert packed.shape == (dim, dim) - -for i in range(dim): - for j in range(i + 1, dim): - assert abs(packed[i, j] - A[i, j]) < 1e-12 - -for i in range(dim): - assert abs(packed[i, i] - T[i, i]) < 1e-12 - if i < dim - 1: - assert abs(packed[i + 1, i] - T[i + 1, i]) < 1e-12 - -A_diag = np.diag(rng.random(dim)) -tri_diag = nanoeigenpy.Tridiagonalization(A_diag) -Q_diag = tri_diag.matrixQ() -T_diag = tri_diag.matrixT() - -assert nanoeigenpy.is_approx(A_diag, Q_diag @ T_diag @ Q_diag.T) -for i in range(dim): - for j in range(dim): - if i != j: - assert abs(T_diag[i, j]) < 1e-10 - -A_tridiag = np.zeros((dim, dim)) -for i in range(dim): - A_tridiag[i, i] = rng.random() - if i < dim - 1: - val = rng.random() - A_tridiag[i, i + 1] = val - A_tridiag[i + 1, i] = val - -tri_tridiag = nanoeigenpy.Tridiagonalization(A_tridiag) -Q_tridiag = tri_tridiag.matrixQ() -T_tridiag = tri_tridiag.matrixT() - -assert nanoeigenpy.is_approx(A_tridiag, Q_tridiag @ T_tridiag @ Q_tridiag.T) - - -tri1_id = nanoeigenpy.Tridiagonalization(dim) -tri2_id = nanoeigenpy.Tridiagonalization(dim) -id1 = tri1_id.id() -id2 = tri2_id.id() -assert id1 != id2 -assert id1 == tri1_id.id() -assert id2 == tri2_id.id() - -tri3_id = nanoeigenpy.Tridiagonalization(A) -tri4_id = nanoeigenpy.Tridiagonalization(A) -id3 = tri3_id.id() -id4 = tri4_id.id() -assert id3 != id4 -assert id3 == tri3_id.id() -assert id4 == tri4_id.id() + +def test_tridiagonalization(): + dim = 100 + rng = np.random.default_rng() + A = rng.random((dim, dim)) + A = (A + A.T) * 0.5 + + tri = nanoeigenpy.Tridiagonalization(A) + + Q = tri.matrixQ() + T = tri.matrixT() + + assert nanoeigenpy.is_approx(A, Q @ T @ Q.T) + assert nanoeigenpy.is_approx(Q @ Q.T, np.eye(dim)) + + for i in range(dim): + for j in range(dim): + if abs(i - j) > 1: + assert abs(T[i, j]) < 1e-12 + + assert nanoeigenpy.is_approx(T, T.T) + + diag = tri.diagonal() + sub_diag = tri.subDiagonal() + + for i in range(dim): + assert abs(diag[i] - T[i, i]) < 1e-12 + + for i in range(dim - 1): + assert abs(sub_diag[i] - T[i + 1, i]) < 1e-12 + + A_test = rng.random((dim, dim)) + A_test = (A_test + A_test.T) * 0.5 + + tri1 = nanoeigenpy.Tridiagonalization(dim) + tri1.compute(A_test) + tri2 = nanoeigenpy.Tridiagonalization(A_test) + + Q1 = tri1.matrixQ() + T1 = tri1.matrixT() + Q2 = tri2.matrixQ() + T2 = tri2.matrixT() + + assert nanoeigenpy.is_approx(Q1, Q2) + assert nanoeigenpy.is_approx(T1, T2) + + h_coeffs = tri.householderCoefficients() + packed = tri.packedMatrix() + + assert h_coeffs.shape == (dim - 1,) + assert packed.shape == (dim, dim) + + for i in range(dim): + for j in range(i + 1, dim): + assert abs(packed[i, j] - A[i, j]) < 1e-12 + + for i in range(dim): + assert abs(packed[i, i] - T[i, i]) < 1e-12 + if i < dim - 1: + assert abs(packed[i + 1, i] - T[i + 1, i]) < 1e-12 + + A_diag = np.diag(rng.random(dim)) + tri_diag = nanoeigenpy.Tridiagonalization(A_diag) + Q_diag = tri_diag.matrixQ() + T_diag = tri_diag.matrixT() + + assert nanoeigenpy.is_approx(A_diag, Q_diag @ T_diag @ Q_diag.T) + for i in range(dim): + for j in range(dim): + if i != j: + assert abs(T_diag[i, j]) < 1e-10 + + A_tridiag = np.zeros((dim, dim)) + for i in range(dim): + A_tridiag[i, i] = rng.random() + if i < dim - 1: + val = rng.random() + A_tridiag[i, i + 1] = val + A_tridiag[i + 1, i] = val + + tri_tridiag = nanoeigenpy.Tridiagonalization(A_tridiag) + Q_tridiag = tri_tridiag.matrixQ() + T_tridiag = tri_tridiag.matrixT() + + assert nanoeigenpy.is_approx(A_tridiag, Q_tridiag @ T_tridiag @ Q_tridiag.T) + + tri1_id = nanoeigenpy.Tridiagonalization(dim) + tri2_id = nanoeigenpy.Tridiagonalization(dim) + id1 = tri1_id.id() + id2 = tri2_id.id() + assert id1 != id2 + assert id1 == tri1_id.id() + assert id2 == tri2_id.id() + + tri3_id = nanoeigenpy.Tridiagonalization(A) + tri4_id = nanoeigenpy.Tridiagonalization(A) + id3 = tri3_id.id() + id4 = tri4_id.id() + assert id3 != id4 + assert id3 == tri3_id.id() + assert id4 == tri4_id.id()