diff --git a/.gersemirc b/.gersemirc index ebecb14..f2f71eb 100644 --- a/.gersemirc +++ b/.gersemirc @@ -1,4 +1,4 @@ -definitions: [./CMakeLists.txt, ./tests] -line_length: 100 +definitions: [./CMakeLists.txt, ./cmake, ./tests] +line_length: 80 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 c902bd8..850a32a 100644 --- a/.github/workflows/macos-linux-windows-pixi.yml +++ b/.github/workflows/macos-linux-windows-pixi.yml @@ -55,6 +55,8 @@ jobs: steps: - uses: actions/checkout@v6 + with: + submodules: recursive - uses: actions/cache@v4 with: @@ -71,23 +73,19 @@ jobs: run: | pixi run -e ${{ matrix.environment }} ccache -z - - name: Configure nanoeigenpy [MacOS/Linux/Windows] + - name: Build 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 }} test + pixi run -e ${{ matrix.environment }} ctest --test-dir build --output-on-failure - name: Install nanoeigenpy [MacOS/Linux/Windows] run: | - pixi run -e ${{ matrix.environment }} install + pixi run -e ${{ matrix.environment }} cmake --build build --target install - name: Show ccache statistics [MacOS/Linux/Windows] run: | @@ -104,12 +102,14 @@ 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: false # ⚠️ Disabling cache for testing ⚠️ + cache: true 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 95d9dee..a5addae 100644 --- a/.github/workflows/ros_ci.yml +++ b/.github/workflows/ros_ci.yml @@ -31,12 +31,11 @@ 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 6aaa80b..36de0b5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,8 @@ build*/ -install*/ -.pytest_cache/ .cache/ -.pixi/ -__pycache__/ -Xcode* -*.pyc -*~ +__pycache__ +.pytest_cache + +# pixi environments +.pixi *.egg-info -.ruff_cache -.DS_Store -compile_commands.json -cmake-profiling.json -result -*.conda diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..955a435 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[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 3ab5117..8a0c6a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,6 @@ 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 cbb34d6..f18aaa6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,241 +1,248 @@ -cmake_minimum_required(VERSION 3.22...4.2) +# +# Copyright 2025 INRIA +# -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" -) - -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) +cmake_minimum_required(VERSION 3.22) -jrl_option(BUILD_WITH_CHOLMOD_SUPPORT - "Build EigenPy with the Cholmod (LGPL) support. See CHOLMOD/Doc/License.txt for further details." OFF -) - -jrl_option(BUILD_WITH_ACCELERATE_SUPPORT - "Build EigenPy with the Accelerate support (Apple only)" OFF +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" ) - -if(BUILD_WITH_ACCELERATE_SUPPORT AND NOT APPLE) - message(WARNING "Accelerate support is only available on APPLE systems") -endif() - -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") +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() -# 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") - - message( - STATUS - "nanobind: fallback to FetchContent from ${nanobind_GIT_REPOSITORY} (tag: ${nanobind_GIT_TAG})" - ) - - include(FetchContent) - FetchContent_Declare( - nanobind - GIT_REPOSITORY ${nanobind_GIT_REPOSITORY} - GIT_TAG ${nanobind_GIT_TAG} - GIT_SHALLOW True - ) - FetchContent_MakeAvailable(nanobind) -endif() +option(INSTALL_DOCUMENTATION "Generate and install the documentation" OFF) -if(BUILD_WITH_CHOLMOD_SUPPORT) - jrl_find_package(CHOLMOD CONFIG REQUIRED) +if(POLICY CMP0167) + cmake_policy(SET CMP0167 NEW) + set(CMAKE_POLICY_DEFAULT_CMP0167 NEW) endif() - -if(BUILD_WITH_ACCELERATE_SUPPORT AND APPLE) - jrl_find_package(Accelerate REQUIRED) +if(POLICY CMP0177) + cmake_policy(SET CMP0177 NEW) + set(CMAKE_POLICY_DEFAULT_CMP0177 NEW) endif() - -if(BUILD_TESTING) - jrl_find_package(Pytest REQUIRED) +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" + ) endif() -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 +option( + BUILD_WITH_CHOLMOD_SUPPORT + "Build NanoEigenPy with the Cholmod support" + OFF ) -if(BUILD_WITH_CHOLMOD_SUPPORT) - list( - APPEND - 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 +if(APPLE) + option( + BUILD_WITH_ACCELERATE_SUPPORT + "Build EigenPy with the Accelerate support" + OFF ) +endif(APPLE) + +# Find dependencies +ADD_PROJECT_DEPENDENCY(Eigen3 REQUIRED PKG_CONFIG_REQUIRES "eigen3 >= 3.3.1") + +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}") endif() -if(BUILD_WITH_ACCELERATE_SUPPORT) - list( - APPEND - nanoeigenpy_HEADERS - include/nanoeigenpy/decompositions/sparse/accelerate/accelerate.hpp - ) +# 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) endif() -# ----------------------------------------------------------------------- # +# Setup main targets +file( + GLOB_RECURSE ${PROJECT_NAME}_HEADERS + CONFIGURE_DEPENDS + include/nanoeigenpy/*.hpp +) add_library(nanoeigenpy_headers INTERFACE) -add_library(nanoeigenpy::nanoeigenpy_headers ALIAS nanoeigenpy_headers) -target_compile_features(nanoeigenpy_headers INTERFACE cxx_std_17) - target_include_directories( nanoeigenpy_headers INTERFACE - $ $ + $ + $ ) +target_link_libraries(nanoeigenpy_headers INTERFACE Eigen3::Eigen) -set(nanoeigenpy_SOURCES src/module.cpp) +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) -# 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) +# 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/.*" + ) +endif(BUILD_WITH_CHOLMOD_SUPPORT) -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) +# 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) -target_link_libraries(nanoeigenpy PRIVATE nanoeigenpy_headers Eigen3::Eigen) +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/.*" + ) +endif(BUILD_WITH_ACCELERATE_SUPPORT) -if(BUILD_WITH_CHOLMOD_SUPPORT) - target_link_libraries(nanoeigenpy PRIVATE SuiteSparse::CHOLMOD) - target_compile_definitions(nanoeigenpy PRIVATE NANOEIGENPY_HAS_CHOLMOD) +if(BUILD_WITH_ACCELERATE_SUPPORT) + target_link_libraries(nanoeigenpy PRIVATE Accelerate) endif() -if(BUILD_WITH_ACCELERATE_SUPPORT AND APPLE) - target_link_libraries(nanoeigenpy PRIVATE Accelerate::Accelerate) - target_compile_definitions(nanoeigenpy PRIVATE NANOEIGENPY_HAS_ACCELERATE) +if(BUILD_TESTING) + add_subdirectory(tests) 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() +nanobind_add_stub( + nanoeigenpy_stub + INSTALL_TIME + VERBOSE + MODULE nanoeigenpy + OUTPUT ${Python_SITELIB}/nanoeigenpy.pyi + PYTHON_PATH $ +) -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 install( - 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 + 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} ) -jrl_add_export_component(NAME nanoeigenpy_headers TARGETS nanoeigenpy_headers) -jrl_export_package() +install( + TARGETS ${PROJECT_NAME} + EXPORT ${TARGETS_EXPORT_NAME} + LIBRARY DESTINATION ${Python_SITELIB} +) -# ------------------------------------------------------------------------ # -if(BUILD_TESTING) - enable_testing() - add_subdirectory(tests) -endif() +ADD_HEADER_GROUP(${PROJECT_NAME}_HEADERS) +ADD_SOURCE_GROUP(${PROJECT_NAME}_SOURCES) -jrl_print_dependencies_summary() -jrl_print_options_summary() +SETUP_PROJECT_FINALIZE() diff --git a/cmake b/cmake new file mode 160000 index 0000000..a3b7cb9 --- /dev/null +++ b/cmake @@ -0,0 +1 @@ +Subproject commit a3b7cb9a4732f4aae34b4c14d72b04c3fa575ed3 diff --git a/cmake/get-jrl-cmakemodules.cmake b/cmake/get-jrl-cmakemodules.cmake deleted file mode 100644 index 1a0a5a0..0000000 --- a/cmake/get-jrl-cmakemodules.cmake +++ /dev/null @@ -1,50 +0,0 @@ -# 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 new file mode 160000 index 0000000..879bca4 --- /dev/null +++ b/ext/nanobind @@ -0,0 +1 @@ +Subproject commit 879bca4869664bdc1446ee7f160ffe3c7028cd7a diff --git a/flake.lock b/flake.lock index a5257f0..25475b1 100644 --- a/flake.lock +++ b/flake.lock @@ -5,11 +5,11 @@ "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1765835352, - "narHash": "sha256-XswHlK/Qtjasvhd1nOa1e8MgZ8GS//jBoTqWtrS1Giw=", + "lastModified": 1754487366, + "narHash": "sha256-pHYj8gUBapuUzKV/kN/tR3Zvqc7o6gdFB9XKXIp1SQ8=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "a34fae9c08a15ad73f295041fec82323541400a9", + "rev": "af66ad14b28a127c5c0f3bbb298218fc63528a18", "type": "github" }, "original": { @@ -18,37 +18,13 @@ "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": 1766153546, - "narHash": "sha256-lIagbRYXHD1DClqzuqmkC3N/GOxZrwTh13eRtmNUyEg=", - "owner": "nim65s", + "lastModified": 1756266583, + "narHash": "sha256-cr748nSmpfvnhqSXPiCfUPxRz2FJnvf/RjJGvFfaCsM=", + "owner": "NixOS", "repo": "nixpkgs", - "rev": "a945047facf45faddcbfa2315207950334c62720", + "rev": "8a6d5427d99ec71c64f0b93d45778c889005d9c2", "type": "github" }, "original": { @@ -60,11 +36,11 @@ }, "nixpkgs-lib": { "locked": { - "lastModified": 1765674936, - "narHash": "sha256-k00uTP4JNfmejrCLJOwdObYC9jHRrr/5M/a/8L2EIdo=", + "lastModified": 1753579242, + "narHash": "sha256-zvaMGVn14/Zz8hnp4VWT9xVnhc8vuL3TStRqwk22biA=", "owner": "nix-community", "repo": "nixpkgs.lib", - "rev": "2075416fcb47225d9b68ac469a5c4801a9c4dd85", + "rev": "0f36c44e01a6129be94e3ade315a5883f0228a6e", "type": "github" }, "original": { @@ -76,7 +52,6 @@ "root": { "inputs": { "flake-parts": "flake-parts", - "jrl-cmakemodules": "jrl-cmakemodules", "nixpkgs": "nixpkgs" } } diff --git a/flake.nix b/flake.nix index e460740..94a6735 100644 --- a/flake.nix +++ b/flake.nix @@ -4,80 +4,30 @@ 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; } ( - { 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 + 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 ]; }; - }; - 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 6f1aab7..30d51b1 100644 --- a/include/nanoeigenpy/decompositions/sparse/sparse-lu.hpp +++ b/include/nanoeigenpy/decompositions/sparse/sparse-lu.hpp @@ -63,13 +63,8 @@ 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 3a08d4e..9546110 100644 --- a/include/nanoeigenpy/fwd.hpp +++ b/include/nanoeigenpy/fwd.hpp @@ -7,22 +7,43 @@ #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 2150488..c04a793 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_9.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/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_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_8.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-h8577fbf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.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-h8577fbf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.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-h8577fbf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.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-h8577fbf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.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_9.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/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_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_8.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-h8577fbf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.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-h8577fbf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.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-h8577fbf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.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-h8577fbf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.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-h8577fbf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.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_9.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/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_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_8.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-h8577fbf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.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-h8577fbf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.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-h8577fbf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.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-h8577fbf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.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_9.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/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_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_8.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-h8577fbf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.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-h8577fbf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.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-h8577fbf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.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-h8577fbf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.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_9.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/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_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_8.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-h8577fbf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.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_9.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/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_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_8.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-h8577fbf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.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-h8577fbf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.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-h8577fbf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.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-h8577fbf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.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,12 +2038,10 @@ 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 @@ -2051,8 +2049,7 @@ 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/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/noarch/kernel-headers_linux-64-4.18.0-he073ed8_8.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 @@ -2092,23 +2089,17 @@ 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_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_8.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/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/noarch/tzdata-2025b-h78e105d_0.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 @@ -2128,16 +2119,13 @@ 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 @@ -2178,12 +2166,8 @@ 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 @@ -2193,10 +2177,8 @@ 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/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/noarch/tzdata-2025b-h78e105d_0.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 @@ -2216,15 +2198,12 @@ 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 @@ -2265,12 +2244,8 @@ 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 @@ -2280,23 +2255,18 @@ 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/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/noarch/tzdata-2025b-h78e105d_0.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 @@ -2322,19 +2292,13 @@ 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/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/noarch/tzdata-2025b-h78e105d_0.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 @@ -2375,7 +2339,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_9.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/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 @@ -2426,11 +2390,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_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_8.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-h8577fbf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.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 @@ -2516,7 +2480,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-h8577fbf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.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 @@ -2601,7 +2565,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-h8577fbf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.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 @@ -2650,7 +2614,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-h8577fbf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.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 @@ -2708,7 +2672,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-h8577fbf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda - conda: . build: hb0f4dca_0 @@ -2750,7 +2714,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-h8577fbf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda - conda: . build: h0dc7051_0 @@ -2791,7 +2755,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-h8577fbf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda - conda: . build: h60d57d3_0 @@ -2826,7 +2790,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-h8577fbf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.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 @@ -3915,15 +3879,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_9.conda - sha256: 41557eeadf641de6aeae49486cef30d02a6912d8da98585d687894afd65b356a - md5: 86d9cba083cd041bfbf242a01a7a1999 +- conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-4.18.0-he073ed8_8.conda + sha256: 305c22a251db227679343fd73bfde121e555d466af86e537847f4c8b9436be0d + md5: ff007ab0f0fdc53d245972bba8a6d40c 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: 1278712 - timestamp: 1765578681495 + size: 1272697 + timestamp: 1752669126073 - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda sha256: 0960d06048a7185d3542d850986d807c6e37ca2e644342dd0c72feefcf26c2a4 md5: b38117a3c920364aff79f870c984b4a3 @@ -7438,17 +7402,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_9.conda - sha256: c47299fe37aebb0fcf674b3be588e67e4afb86225be4b0d452c7eb75c086b851 - md5: 13dc3adbc692664cd3beabd216434749 +- conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_8.conda + sha256: 0053c17ffbd9f8af1a7f864995d70121c292e317804120be4667f37c92805426 + md5: 1bad93f0aa428d618875ef3a588a889e depends: - __glibc >=2.28 - - kernel-headers_linux-64 4.18.0 he073ed8_9 + - kernel-headers_linux-64 4.18.0 he073ed8_8 - 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: 24008591 - timestamp: 1765578833462 + size: 24210909 + timestamp: 1752669140965 - conda: https://conda.anaconda.org/conda-forge/osx-64/tapi-1600.0.11.8-h8d8e812_0.conda sha256: 2602632f7923fd59042a897bfb22f050d78f2b5960d53565eae5fa6a79308caa md5: aae272355bc3f038e403130a5f6f5495 @@ -7554,12 +7518,12 @@ packages: license_family: PSF size: 51692 timestamp: 1756220668932 -- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h8577fbf_1.conda - sha256: 865716d3e2ccaca1218462645830d2370ab075a9a118c238728e1231a234bc6c - md5: e4e8496b68cf5f25e76fbe67f3856550 +- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + sha256: 5aaa366385d716557e365f0a4e9c3fca43ba196872abbbe3d56bb610d131e192 + md5: 4222072737ccff51314b5ece9c7d6f5a license: LicenseRef-Public-Domain - size: 119010 - timestamp: 1765580300078 + size: 122968 + timestamp: 1742727099393 - 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 2359319..4142b97 100644 --- a/pixi.toml +++ b/pixi.toml @@ -22,12 +22,6 @@ 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" @@ -49,7 +43,6 @@ 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 @@ -84,42 +77,13 @@ 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", depends-on = ["configure"] } +build = { cmd = "cmake --build build --target all", 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] @@ -171,8 +135,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] @@ -184,8 +148,8 @@ dependencies = { clangxx = "*", lld = "*" } dependencies = { nanoeigenpy = { path = "." }, cmake = ">=3.22", python = "*" } [feature.test-pixi-build.tasks] -test-cmake = "cmake -S tests/packaging/cmake -B build/test_pixi_build" -test-python = "python -c 'import nanoeigenpy; print(nanoeigenpy.__version__)'" +test-cmake = "cmake -S tests/packaging/pixi_build -B build_test_pixi_build" +test-python = "python -c 'import nanoeigenpy'" test = { depends-on = ["test-cmake", "test-python"] } [environments] diff --git a/src/internal.h b/src/internal.h new file mode 100644 index 0000000..ea9cbe9 --- /dev/null +++ b/src/internal.h @@ -0,0 +1,9 @@ +#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 2d12755..3dfd59a 100644 --- a/src/module.cpp +++ b/src/module.cpp @@ -2,19 +2,13 @@ #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" -namespace nb = nanobind; - -using Scalar = double; -static constexpr int Options = Eigen::ColMajor; -using Matrix = Eigen::Matrix; -using Vector = Eigen::Matrix; +#include "./internal.h" using namespace nanoeigenpy; @@ -37,13 +31,9 @@ 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; -#endif + typename Eigen::MappedSparseMatrix; + NB_MAKE_OPAQUE(ColPivHhJacobiSVD) NB_MAKE_OPAQUE(FullPivHhJacobiSVD) NB_MAKE_OPAQUE(HhJacobiSVD) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 06b5446..18cfe81 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,58 +1,108 @@ -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) +# Copyright 2025 INRIA -function(nanoeigenpy_add_test name) - set(test_name "nanoeigenpy: ${name}") - add_test( - NAME ${test_name} - COMMAND $ ${CMAKE_CURRENT_SOURCE_DIR}/test_${name}.py +# 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 ) - set_property( - TEST ${test_name} - PROPERTY - ENVIRONMENT_MODIFICATION - "PYTHONPATH=path_list_prepend:$" - "PYTHONPATH=path_list_prepend:$" + # 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}) + + add_test( + NAME "${PROJECT_NAME}-import-${name}" + COMMAND ${Python_EXECUTABLE} -c "import ${name}" + WORKING_DIRECTORY $ ) endfunction() -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) +# 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 +) if(BUILD_WITH_CHOLMOD_SUPPORT) - nanoeigenpy_add_test(cholmod_simplicial_ldlt) - nanoeigenpy_add_test(cholmod_simplicial_llt) - nanoeigenpy_add_test(cholmod_supernodal_llt) -endif() + 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() if(BUILD_WITH_ACCELERATE_SUPPORT) - nanoeigenpy_add_test(accelerate) -endif() + message(STATUS "Adding Python test test_accelerate") + add_tests_py_module(test_accelerate) +endif(BUILD_WITH_ACCELERATE_SUPPORT) diff --git a/tests/packaging/cmake/CMakeLists.txt b/tests/packaging/pixi_build/CMakeLists.txt similarity index 61% rename from tests/packaging/cmake/CMakeLists.txt rename to tests/packaging/pixi_build/CMakeLists.txt index 6971366..46e4847 100644 --- a/tests/packaging/cmake/CMakeLists.txt +++ b/tests/packaging/pixi_build/CMakeLists.txt @@ -1,4 +1,3 @@ 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 f02eebd..d21d46d 100644 --- a/tests/test_accelerate.py +++ b/tests/test_accelerate.py @@ -1,23 +1,11 @@ import nanoeigenpy import numpy as np from scipy.sparse import csc_matrix -import pytest rng = np.random.default_rng() -@pytest.mark.parametrize( - "SolverType", - [ - nanoeigenpy.AccelerateLLT, - nanoeigenpy.AccelerateLDLT, - nanoeigenpy.AccelerateLDLTUnpivoted, - nanoeigenpy.AccelerateLDLTSBK, - nanoeigenpy.AccelerateLDLTTPP, - nanoeigenpy.AccelerateQR, - ], -) -def test_accelerate_solver(SolverType): +def test(SolverType: type): dim = 100 A = rng.random((dim, dim)) A = (A + A.T) * 0.5 + np.diag(10.0 + rng.random(dim)) @@ -37,3 +25,11 @@ def test_accelerate_solver(SolverType): 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 3532467..cb1b050 100644 --- a/tests/test_cholmod_simplicial_ldlt.py +++ b/tests/test_cholmod_simplicial_ldlt.py @@ -1,25 +1,24 @@ -import nanoeigenpy import numpy as np from scipy.sparse import csc_matrix +import nanoeigenpy -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)) +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 2b8137c..763cfaa 100644 --- a/tests/test_cholmod_simplicial_llt.py +++ b/tests/test_cholmod_simplicial_llt.py @@ -1,26 +1,25 @@ -import nanoeigenpy import numpy as np from scipy.sparse import csc_matrix +import nanoeigenpy -def test_cholmod_simplicial_llt(): - dim = 100 - rng = np.random.default_rng() +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 c938531..15de556 100644 --- a/tests/test_cholmod_supernodal_llt.py +++ b/tests/test_cholmod_supernodal_llt.py @@ -1,26 +1,25 @@ -import nanoeigenpy import numpy as np from scipy.sparse import csc_matrix +import nanoeigenpy -def test_cholmod_supernodal_llt(): - dim = 100 - rng = np.random.default_rng() +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 3b3d85a..df6527a 100644 --- a/tests/test_complex_eigen_solver.py +++ b/tests/test_complex_eigen_solver.py @@ -1,34 +1,32 @@ import nanoeigenpy import numpy as np +dim = 100 +rng = np.random.default_rng() +A = rng.random((dim, dim)) -def test_complex_eigen_solver(): - dim = 100 - rng = np.random.default_rng() - A = rng.random((dim, dim)) +es = nanoeigenpy.ComplexEigenSolver(A) +assert es.info() == nanoeigenpy.ComputationInfo.Success - es = nanoeigenpy.ComplexEigenSolver(A) - assert es.info() == nanoeigenpy.ComputationInfo.Success +V = es.eigenvectors() +D = es.eigenvalues() +assert V.shape == (dim, dim) +assert D.shape == (dim,) - V = es.eigenvectors() - D = es.eigenvalues() - assert V.shape == (dim, dim) - assert D.shape == (dim,) +AV = A @ V +VD = V @ np.diag(D) +assert nanoeigenpy.is_approx(AV.real, VD.real) +assert nanoeigenpy.is_approx(AV.imag, VD.imag) - AV = A @ V - VD = V @ np.diag(D) - assert nanoeigenpy.is_approx(AV.real, VD.real) - assert nanoeigenpy.is_approx(AV.imag, VD.imag) +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 - 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() +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 55a671b..da1acb9 100644 --- a/tests/test_complex_schur.py +++ b/tests/test_complex_schur.py @@ -1,51 +1,49 @@ import nanoeigenpy import numpy as np +dim = 100 +rng = np.random.default_rng() +A = rng.random((dim, dim)) -def test_complex_schur(): - dim = 100 - rng = np.random.default_rng() - A = rng.random((dim, dim)) +cs = nanoeigenpy.ComplexSchur(A) +assert cs.info() == nanoeigenpy.ComputationInfo.Success - cs = nanoeigenpy.ComplexSchur(A) - assert cs.info() == nanoeigenpy.ComputationInfo.Success +U = cs.matrixU() +T = cs.matrixT() - U = cs.matrixU() - T = cs.matrixT() +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_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)) +for row in range(1, dim): + for col in range(row): + assert abs(T[row, col]) < 1e-12 - for row in range(1, dim): - for col in range(row): - assert abs(T[row, col]) < 1e-12 +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 = 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 +T_triangular = cs_triangular.matrixT() +U_triangular = cs_triangular.matrixU() - T_triangular = cs_triangular.matrixT() - U_triangular = cs_triangular.matrixU() +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)) - 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)) +hess = nanoeigenpy.HessenbergDecomposition(A) +H = hess.matrixH() +Q_hess = hess.matrixQ() - hess = nanoeigenpy.HessenbergDecomposition(A) - H = hess.matrixH() - Q_hess = hess.matrixQ() +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 - 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 +T_from_hess = cs_from_hess.matrixT() +U_from_hess = cs_from_hess.matrixU() - 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 - ) +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 38f8233..27a9594 100644 --- a/tests/test_eigen_solver.py +++ b/tests/test_eigen_solver.py @@ -1,42 +1,40 @@ import nanoeigenpy import numpy as np +dim = 100 +rng = np.random.default_rng() -def test_eigen_solver(): - dim = 100 - rng = np.random.default_rng() +A = rng.random((dim, dim)) - A = rng.random((dim, dim)) +es = nanoeigenpy.EigenSolver() +es = nanoeigenpy.EigenSolver(dim) +es = nanoeigenpy.EigenSolver(A) +assert es.info() == nanoeigenpy.ComputationInfo.Success - es = nanoeigenpy.EigenSolver() - es = nanoeigenpy.EigenSolver(dim) - es = nanoeigenpy.EigenSolver(A) - assert es.info() == nanoeigenpy.ComputationInfo.Success +V = es.eigenvectors() +D = es.eigenvalues() - V = es.eigenvectors() - D = es.eigenvalues() +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) - 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) +es1 = nanoeigenpy.EigenSolver() +es2 = nanoeigenpy.EigenSolver() - es1 = nanoeigenpy.EigenSolver() - es2 = nanoeigenpy.EigenSolver() +id1 = es1.id() +id2 = es2.id() - id1 = es1.id() - id2 = es2.id() +assert id1 != id2 +assert id1 == es1.id() +assert id2 == es2.id() - assert id1 != id2 - assert id1 == es1.id() - assert id2 == es2.id() +dim_constructor = 3 - dim_constructor = 3 +es3 = nanoeigenpy.EigenSolver(dim_constructor) +es4 = nanoeigenpy.EigenSolver(dim_constructor) - es3 = nanoeigenpy.EigenSolver(dim_constructor) - es4 = nanoeigenpy.EigenSolver(dim_constructor) +id3 = es3.id() +id4 = es4.id() - id3 = es3.id() - id4 = es4.id() - - assert id3 != id4 - assert id3 == es3.id() - assert 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 25b1c0e..94e5dcc 100644 --- a/tests/test_full_piv_lu.py +++ b/tests/test_full_piv_lu.py @@ -1,111 +1,109 @@ import nanoeigenpy import numpy as np - -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() +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() diff --git a/tests/test_generalized_eigen_solver.py b/tests/test_generalized_eigen_solver.py index 2bf2c01..4be542c 100644 --- a/tests/test_generalized_eigen_solver.py +++ b/tests/test_generalized_eigen_solver.py @@ -1,42 +1,40 @@ import nanoeigenpy import numpy as np - -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 +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 7097178..1fe48ab 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -1,7 +1,7 @@ -import nanoeigenpy -import quaternion import numpy as np from numpy import cos +import nanoeigenpy +import quaternion verbose = True @@ -14,699 +14,692 @@ def isapprox(a, b, epsilon=1e-6): return abs(a - b) < epsilon -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)) +# --- 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("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]) - ) - 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]) + 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)], + ] ) - 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) + assert isapprox(matrix, expected_matrix) - 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) +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) + assert isapprox(j.s, 0.0) diff --git a/tests/test_hessenberg_decomposition.py b/tests/test_hessenberg_decomposition.py index a82d8d8..9a09802 100644 --- a/tests/test_hessenberg_decomposition.py +++ b/tests/test_hessenberg_decomposition.py @@ -1,72 +1,70 @@ import nanoeigenpy import numpy as np - -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() +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() diff --git a/tests/test_import_extension.py b/tests/test_import_extension.py deleted file mode 100644 index 56b01bb..0000000 --- a/tests/test_import_extension.py +++ /dev/null @@ -1,5 +0,0 @@ -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 3bd74b0..01f4f11 100644 --- a/tests/test_incomplete_cholesky.py +++ b/tests/test_incomplete_cholesky.py @@ -1,72 +1,71 @@ -import nanoeigenpy import numpy as np from scipy.sparse import csc_matrix +import nanoeigenpy -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 +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 16afeb2..4405e93 100644 --- a/tests/test_incomplete_lut.py +++ b/tests/test_incomplete_lut.py @@ -1,51 +1,49 @@ -import nanoeigenpy import numpy as np from scipy.sparse import csc_matrix +import nanoeigenpy - -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 +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 dd14c39..8d0f066 100644 --- a/tests/test_jacobi_svd.py +++ b/tests/test_jacobi_svd.py @@ -111,3 +111,9 @@ 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 9a2484b..d844863 100644 --- a/tests/test_ldlt.py +++ b/tests/test_ldlt.py @@ -1,98 +1,96 @@ import nanoeigenpy import numpy as np - -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() +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() diff --git a/tests/test_llt.py b/tests/test_llt.py index dd0e6c9..f2b953d 100644 --- a/tests/test_llt.py +++ b/tests/test_llt.py @@ -1,82 +1,80 @@ import nanoeigenpy import numpy as np +dim = 100 +rng = np.random.default_rng() -def test_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)) +llt = nanoeigenpy.LLT(A) - llt = nanoeigenpy.LLT(A) +assert llt.info() == nanoeigenpy.ComputationInfo.Success - assert llt.info() == nanoeigenpy.ComputationInfo.Success +L = llt.matrixL() +assert nanoeigenpy.is_approx(L.dot(np.transpose(L)), A) - L = llt.matrixL() - assert nanoeigenpy.is_approx(L.dot(np.transpose(L)), A) +U = llt.matrixU() +LU = L @ U +assert nanoeigenpy.is_approx(LU, A) - U = llt.matrixU() - LU = L @ U - assert nanoeigenpy.is_approx(LU, 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) - 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) +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) - 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) - LLT = llt.matrixLLT() - LLT_lower = np.tril(LLT) - assert nanoeigenpy.is_approx(LLT_lower, L) +A_upper = np.triu(A, k=1) +LLT_upper = np.triu(LLT, k=1) +assert nanoeigenpy.is_approx(A_upper, LLT_upper) - A_upper = np.triu(A, k=1) - LLT_upper = np.triu(LLT, k=1) - assert nanoeigenpy.is_approx(A_upper, LLT_upper) +A_reconstructed = llt.reconstructedMatrix() +assert nanoeigenpy.is_approx(A_reconstructed, A) - A_reconstructed = llt.reconstructedMatrix() - assert nanoeigenpy.is_approx(A_reconstructed, A) +adjoint = llt.adjoint() +assert adjoint is llt - adjoint = llt.adjoint() - assert adjoint is llt +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 - 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 +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)) - 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)) +llt1 = nanoeigenpy.LLT() +llt2 = nanoeigenpy.LLT() - llt1 = nanoeigenpy.LLT() - llt2 = nanoeigenpy.LLT() +id1 = llt1.id() +id2 = llt2.id() - id1 = llt1.id() - id2 = llt2.id() +assert id1 != id2 +assert id1 == llt1.id() +assert id2 == llt2.id() - assert id1 != id2 - assert id1 == llt1.id() - assert id2 == llt2.id() +dim_constructor = 3 - dim_constructor = 3 +llt3 = nanoeigenpy.LLT(dim_constructor) +llt4 = nanoeigenpy.LLT(dim_constructor) - llt3 = nanoeigenpy.LLT(dim_constructor) - llt4 = nanoeigenpy.LLT(dim_constructor) +id3 = llt3.id() +id4 = llt4.id() - id3 = llt3.id() - id4 = llt4.id() - - assert id3 != id4 - assert id3 == llt3.id() - assert 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 1e6e5ee..98554c6 100644 --- a/tests/test_partial_piv_lu.py +++ b/tests/test_partial_piv_lu.py @@ -1,77 +1,75 @@ 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) -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, 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) - 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) +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) - 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 - rows = partialpivlu.rows() - cols = partialpivlu.cols() - assert cols == dim - assert rows == dim +partialpivlu_compute = partialpivlu.compute(A) +A_reconstructed = partialpivlu.reconstructedMatrix() +assert nanoeigenpy.is_approx(A_reconstructed, A) - partialpivlu_compute = partialpivlu.compute(A) # noqa - A_reconstructed = partialpivlu.reconstructedMatrix() - assert nanoeigenpy.is_approx(A_reconstructed, A) +LU = partialpivlu.matrixLU() +P_perm = partialpivlu.permutationP() +P = P_perm.toDenseMatrix() - LU = partialpivlu.matrixLU() - P_perm = partialpivlu.permutationP() - P = P_perm.toDenseMatrix() +U = np.triu(LU) +L = np.eye(dim) + np.tril(LU, -1) +assert nanoeigenpy.is_approx(P @ A, L @ U) - U = np.triu(LU) - L = np.eye(dim) + np.tril(LU, -1) - assert nanoeigenpy.is_approx(P @ A, L @ U) +inverse = partialpivlu.inverse() +assert nanoeigenpy.is_approx(A @ inverse, np.eye(dim)) +assert nanoeigenpy.is_approx(inverse @ A, np.eye(dim)) - inverse = partialpivlu.inverse() - assert nanoeigenpy.is_approx(A @ inverse, np.eye(dim)) - assert nanoeigenpy.is_approx(inverse @ A, 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 - 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 +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)) - 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)) +decomp1 = nanoeigenpy.PartialPivLU() +decomp2 = nanoeigenpy.PartialPivLU() +id1 = decomp1.id() +id2 = decomp2.id() +assert id1 != id2 +assert id1 == decomp1.id() +assert id2 == decomp2.id() - decomp1 = nanoeigenpy.PartialPivLU() - decomp2 = nanoeigenpy.PartialPivLU() - id1 = decomp1.id() - id2 = decomp2.id() - assert id1 != id2 - assert id1 == decomp1.id() - assert id2 == decomp2.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() - 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() +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 95c74f6..29e76f8 100644 --- a/tests/test_permutation_matrix.py +++ b/tests/test_permutation_matrix.py @@ -1,61 +1,59 @@ import nanoeigenpy import numpy as np +dim = 100 +rng = np.random.default_rng() +indices = rng.permutation(dim) -def test_permutation_matrix(): - dim = 100 - rng = np.random.default_rng() - indices = rng.permutation(dim) +perm = nanoeigenpy.PermutationMatrix(dim) +perm = nanoeigenpy.PermutationMatrix(indices) - perm = nanoeigenpy.PermutationMatrix(dim) - perm = nanoeigenpy.PermutationMatrix(indices) +est_indices = perm.indices() +assert est_indices.all() == indices.all() - est_indices = perm.indices() - assert est_indices.all() == indices.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_left = perm.applyTranspositionOnTheLeft(0, 1) - perm_left_right = perm_left.applyTranspositionOnTheRight(0, 1) - assert perm_left_right.indices().all() == perm.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.setIdentity() - assert perm.indices().all() == np.arange(dim).all() - dim = dim + 1 - perm.setIdentity(dim) - assert perm.indices().all() == np.arange(dim).all() +perm.setIdentity() +dense = perm.toDenseMatrix() +assert dense.all() == np.eye(dim).all() - perm.setIdentity() - dense = perm.toDenseMatrix() - assert dense.all() == np.eye(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 = 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_inv = perm.inverse() +result = perm * perm_inv +identity = result.toDenseMatrix() +assert identity.all() == np.eye(3).all() - perm_inv = perm.inverse() - result = perm * perm_inv - identity = result.toDenseMatrix() - assert identity.all() == np.eye(3).all() +dim_constructor = 3 - dim_constructor = 3 +perm1 = nanoeigenpy.PermutationMatrix(dim_constructor) +perm2 = nanoeigenpy.PermutationMatrix(dim_constructor) - perm1 = nanoeigenpy.PermutationMatrix(dim_constructor) - perm2 = nanoeigenpy.PermutationMatrix(dim_constructor) +id1 = perm1.id() +id2 = perm2.id() - id1 = perm1.id() - id2 = perm2.id() +assert id1 != id2 +assert id1 == perm1.id() +assert id2 == perm2.id() - assert id1 != id2 - assert id1 == perm1.id() - assert id2 == perm2.id() +es3 = nanoeigenpy.PermutationMatrix(indices) +es4 = nanoeigenpy.PermutationMatrix(indices) - es3 = nanoeigenpy.PermutationMatrix(indices) - es4 = nanoeigenpy.PermutationMatrix(indices) +id3 = es3.id() +id4 = es4.id() - id3 = es3.id() - id4 = es4.id() - - assert id3 != id4 - assert id3 == es3.id() - assert 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 5dacbbd..fa0f194 100644 --- a/tests/test_qr.py +++ b/tests/test_qr.py @@ -1,102 +1,100 @@ import nanoeigenpy import numpy as np - -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 +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 diff --git a/tests/test_real_qz.py b/tests/test_real_qz.py index e778f05..088708d 100644 --- a/tests/test_real_qz.py +++ b/tests/test_real_qz.py @@ -1,39 +1,37 @@ import nanoeigenpy import numpy as np - -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() +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 9e9efe7..399675a 100644 --- a/tests/test_real_schur.py +++ b/tests/test_real_schur.py @@ -2,50 +2,47 @@ import numpy as np -def test_real_schur(): - def verify_is_quasi_triangular(T): - size = T.shape[0] +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)) - rs = nanoeigenpy.RealSchur(A) - assert rs.info() == nanoeigenpy.ComputationInfo.Success +dim = 100 +rng = np.random.default_rng() +A = rng.random((dim, dim)) - U = rs.matrixU() - T = rs.matrixT() +rs = nanoeigenpy.RealSchur(A) +assert rs.info() == nanoeigenpy.ComputationInfo.Success - assert nanoeigenpy.is_approx(A, U @ T @ U.T) - assert nanoeigenpy.is_approx(U @ U.T, np.eye(dim)) +U = rs.matrixU() +T = rs.matrixT() - verify_is_quasi_triangular(T) +assert nanoeigenpy.is_approx(A, U @ T @ U.T) +assert nanoeigenpy.is_approx(U @ U.T, np.eye(dim)) - hess = nanoeigenpy.HessenbergDecomposition(A) - H = hess.matrixH() - Q_hess = hess.matrixQ() +verify_is_quasi_triangular(T) - 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 +hess = nanoeigenpy.HessenbergDecomposition(A) +H = hess.matrixH() +Q_hess = hess.matrixQ() - T_from_hess = rs_from_hess.matrixT() - U_from_hess = rs_from_hess.matrixU() +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 - assert nanoeigenpy.is_approx(A, U_from_hess @ T_from_hess @ U_from_hess.T) +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) diff --git a/tests/test_self_adjoint_eigen_solver.py b/tests/test_self_adjoint_eigen_solver.py index ae03b73..d53ed89 100644 --- a/tests/test_self_adjoint_eigen_solver.py +++ b/tests/test_self_adjoint_eigen_solver.py @@ -1,22 +1,20 @@ import nanoeigenpy import numpy as np +dim = 100 +rng = np.random.default_rng() -def test_self_adjoint_eigen_solver(): - dim = 100 - rng = np.random.default_rng() +A = rng.random((dim, dim)) +A = (A + A.T) * 0.5 - A = rng.random((dim, dim)) - A = (A + A.T) * 0.5 +es = nanoeigenpy.SelfAdjointEigenSolver(A) - es = nanoeigenpy.SelfAdjointEigenSolver(A) +assert es.info() == nanoeigenpy.ComputationInfo.Success - assert es.info() == nanoeigenpy.ComputationInfo.Success +V = es.eigenvectors() +D = es.eigenvalues() - V = es.eigenvectors() - D = es.eigenvalues() +AdotV = A @ V +VdotD = V @ np.diag(D) - AdotV = A @ V - VdotD = V @ np.diag(D) - - assert nanoeigenpy.is_approx(AdotV, VdotD, 1e-6) +assert nanoeigenpy.is_approx(AdotV, VdotD, 1e-6) diff --git a/tests/test_simplicial_llt.py b/tests/test_simplicial_llt.py index d613fb7..12e8d46 100644 --- a/tests/test_simplicial_llt.py +++ b/tests/test_simplicial_llt.py @@ -2,47 +2,45 @@ import numpy as np import scipy.sparse as spa - -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()) +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 5c5fa72..859b1f7 100644 --- a/tests/test_sparse_lu.py +++ b/tests/test_sparse_lu.py @@ -2,64 +2,62 @@ import numpy as np import scipy.sparse as spa - -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) +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 9544cf0..7a825f8 100644 --- a/tests/test_sparse_qr.py +++ b/tests/test_sparse_qr.py @@ -2,74 +2,72 @@ import numpy as np import scipy.sparse as spa - -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) +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 9a6d8b5..7b8a2f7 100644 --- a/tests/test_tridiagonalization.py +++ b/tests/test_tridiagonalization.py @@ -1,104 +1,103 @@ import nanoeigenpy import numpy as np - -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() +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()