From 564f9241b65ba845000d1d703d7d721b7e23b9ab Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Wed, 9 Jul 2025 09:21:42 +0900 Subject: [PATCH 01/32] test using cibuildwheel manually --- .github/workflows/wheels.yml | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index e95f803..b1c9261 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -1,8 +1,6 @@ name: Build on: push: - branches: - - main release: types: - created @@ -59,11 +57,23 @@ jobs: # cflags: '' steps: - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + id: python + with: + python-version: "3.11 - 3.13" + update-environment: false + - name: Build wheels - uses: pypa/cibuildwheel@v3.0.1 env: CIBW_PLATFORM: ${{ matrix.platform || 'auto' }} CFLAGS: ${{ matrix.cibw.cflags }} + CIBW_BEFORE_ALL: 'uname -a' + run: | + pip install --upgrade pip + pip install cibuildwheel==3.0.1 + mkdir wheelhouse + cibuildwheel --output-dir wheelhouse + ls wheelhouse - uses: actions/upload-artifact@v4 with: name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }} From ed78185b1dbbdd075d0cdd82cd2e387daef19a89 Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Wed, 9 Jul 2025 09:30:08 +0900 Subject: [PATCH 02/32] remove invalid env --- .github/workflows/wheels.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index b1c9261..d201114 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -26,8 +26,6 @@ jobs: build_wheels: name: Build wheels on ${{ matrix.os }} runs-on: ${{ matrix.os }} - env: - CIBW_ENVIRONMENT: "${{ matrix.cibw.env || '' }}" strategy: matrix: include: From be8dde2f4a20f8e431a39ae4587b9cc8697c0fb1 Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Wed, 9 Jul 2025 09:34:30 +0900 Subject: [PATCH 03/32] os -> runs-on --- .github/workflows/wheels.yml | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index d201114..974028c 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -25,7 +25,7 @@ jobs: path: dist/*.tar.gz build_wheels: name: Build wheels on ${{ matrix.os }} - runs-on: ${{ matrix.os }} + runs-on: ${{ matrix.runs-on }} strategy: matrix: include: @@ -37,22 +37,6 @@ jobs: runs-on: ubuntu-24.04-arm cibw: cflags: '' -# - os: windows-intel -# runs-on: windows-latest -# cibw: -# cflags: '' -# - os: windows-arm -# runs-on: windows-11-arm -# cibw: -# cflags: '' -# - os: macos-intel -# runs-on: macos-13 -# cibw: -# cflags: '' -# - os: macos-arm -# runs-on: macos-latest -# cibw: -# cflags: '' steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 From b4bdae06f6271dd4d29c9412dad03258c1446372 Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Wed, 9 Jul 2025 09:36:50 +0900 Subject: [PATCH 04/32] resurrect original --- .github/workflows/wheels.yml | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 974028c..efff8dc 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -37,25 +37,29 @@ jobs: runs-on: ubuntu-24.04-arm cibw: cflags: '' + - os: windows-intel + runs-on: windows-latest + cibw: + cflags: '' + - os: windows-arm + runs-on: windows-11-arm + cibw: + cflags: '' + - os: macos-intel + runs-on: macos-13 + cibw: + cflags: '' + - os: macos-arm + runs-on: macos-latest + cibw: + cflags: '' steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - id: python - with: - python-version: "3.11 - 3.13" - update-environment: false - - name: Build wheels + uses: pypa/cibuildwheel@v3.0.1 env: CIBW_PLATFORM: ${{ matrix.platform || 'auto' }} CFLAGS: ${{ matrix.cibw.cflags }} - CIBW_BEFORE_ALL: 'uname -a' - run: | - pip install --upgrade pip - pip install cibuildwheel==3.0.1 - mkdir wheelhouse - cibuildwheel --output-dir wheelhouse - ls wheelhouse - uses: actions/upload-artifact@v4 with: name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }} From f3cfc4909de40b291257b8560fe26e36ab1e0aa1 Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Wed, 9 Jul 2025 09:44:36 +0900 Subject: [PATCH 05/32] Omit windows-arm for now (eigen related error) --- .github/workflows/wheels.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index efff8dc..4211ecc 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -41,10 +41,6 @@ jobs: runs-on: windows-latest cibw: cflags: '' - - os: windows-arm - runs-on: windows-11-arm - cibw: - cflags: '' - os: macos-intel runs-on: macos-13 cibw: From 5063926512ffcc3d9825b0d5a1b3ca02c2b22dc0 Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Wed, 9 Jul 2025 10:09:40 +0900 Subject: [PATCH 06/32] downaload-artifact v4 --- .github/workflows/wheels.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 4211ecc..a32d3e6 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -64,9 +64,8 @@ jobs: needs: [build_wheels, build_sdist] runs-on: ubuntu-latest steps: - - uses: actions/download-artifact@v2 + - uses: actions/download-artifact@v4 with: - name: artifact path: dist - name: Publish package to TestPyPI uses: pypa/gh-action-pypi-publish@master From 5ed4452b14fa2640896fa59d3fb5f03be0219a96 Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Wed, 9 Jul 2025 11:20:46 +0900 Subject: [PATCH 07/32] use trusted publisher of pypi --- .github/workflows/wheels.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index a32d3e6..b734726 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -63,25 +63,25 @@ jobs: upload_pypi: needs: [build_wheels, build_sdist] runs-on: ubuntu-latest + permissions: + id-token: write steps: - uses: actions/download-artifact@v4 with: + pattern: cibw-* path: dist + merge-multiple: true - name: Publish package to TestPyPI - uses: pypa/gh-action-pypi-publish@master + uses: pypa/gh-action-pypi-publish@release/v1 with: - user: __token__ - password: ${{ secrets.TEST_PYPI_APITOKEN }} packages_dir: dist/ repository_url: https://test.pypi.org/legacy/ verbose: true skip_existing: true - name: Publish package to PyPI if: github.event_name == 'release' - uses: pypa/gh-action-pypi-publish@master + uses: pypa/gh-action-pypi-publish@release/v1 with: - user: __token__ - password: ${{ secrets.PYPI_APITOKEN }} packages_dir: dist/ verbose: true skip_existing: true From 6c5c66a5c21136b361ca1422f8d062acfcf8b3d6 Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Thu, 10 Jul 2025 11:22:09 +0900 Subject: [PATCH 08/32] working on nanobind --- .gitmodules | 3 + CMakeLists.txt | 48 +++++-- cpp_source/als/definitions.hpp | 1 - cpp_source/als/wrapper.cpp | 149 +++++++++----------- cpp_source/evaluator.cpp | 63 +++++---- cpp_source/knn/knn.hpp | 1 - cpp_source/knn/wrapper.cpp | 60 ++++---- cpp_source/util.cpp | 41 +++--- ext/nanobind | 1 + pyproject.toml | 41 ++++-- setup.py | 48 +------ src/irspack/__init__.py | 7 +- src/irspack/evaluation/__init__.py | 2 +- src/irspack/evaluation/_core.pyi | 55 -------- src/irspack/evaluation/evaluate_df_to_df.py | 2 +- src/irspack/evaluation/evaluator.py | 2 +- src/irspack/recommenders/ials.py | 6 +- 17 files changed, 229 insertions(+), 301 deletions(-) create mode 100644 .gitmodules create mode 160000 ext/nanobind delete mode 100644 src/irspack/evaluation/_core.pyi diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..539e4f6 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "ext/nanobind"] + path = ext/nanobind + url = https://github.com/wjakob/nanobind diff --git a/CMakeLists.txt b/CMakeLists.txt index 0cb2047..3e48f12 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,20 +1,46 @@ -cmake_minimum_required(VERSION 3.0.0) -project(rs_evaluation VERSION 0.1.0) +cmake_minimum_required(VERSION 3.15...3.27) +project(irspack VERSION 0.1.0) + +if (CMAKE_VERSION VERSION_LESS 3.18) + set(DEV_MODULE Development) +else() + set(DEV_MODULE Development.Module) +endif() + + + set(CMAKE_BUILD_TYPE DEBUG) -set(CMAKE_CXX_FLAGS "-std=c++11 -march=native -fPIC -O0 -g") + +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() find_package(Threads REQUIRED) -include_directories(eigen-3.3.7 cpp_source) +include_directories(eigen-3.4.0 cpp_source) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) - set(CPACK_PROJECT_NAME ${PROJECT_NAME}) set(CPACK_PROJECT_VERSION ${PROJECT_VERSION}) include(CPack) -add_subdirectory(pybind11) -pybind11_add_module(_ials cpp_source/als/wrapper.cpp) -#pybind11_add_module(irspack.recommenders._knn cpp_source/knn/wrapper.cpp) -#pybind11_add_module(_util_cpp cpp_source/util.cpp) -#pybind11_add_module(irspack._evapuator cpp_source/evaluator.cpp) -#pybind11_add_module(irspack._rwr cpp_source/rws.cpp) + +# Detect the installed nanobind package and import it into CMake +find_package(Python 3.8 COMPONENTS Interpreter ${DEV_MODULE} REQUIRED) +add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/ext/nanobind) + +nanobind_add_module(_ials_core cpp_source/als/wrapper.cpp) +nanobind_add_module(irspack.recommenders._knn cpp_source/knn/wrapper.cpp) +nanobind_add_module(irspack.utils._util_cpp cpp_source/util.cpp) +nanobind_add_module(_core_evaluator cpp_source/evaluator.cpp) + +install(TARGETS _ials_core LIBRARY DESTINATION irspack/recommenders) +install(TARGETS _core_evaluator LIBRARY DESTINATION irspack/evaluation) + +#nanobind_add_stub( +# _ials_core_stub +# MODULE irspack.recommenders._ials_core +# OUTPUT irspack.recommenders._ials_core.pyi +# PYTHON_PATH "./src" +# DEPENDS _ials_core +#) \ No newline at end of file diff --git a/cpp_source/als/definitions.hpp b/cpp_source/als/definitions.hpp index ca975ab..2705fef 100644 --- a/cpp_source/als/definitions.hpp +++ b/cpp_source/als/definitions.hpp @@ -1,6 +1,5 @@ #include #include -#include namespace irspack { namespace ials { diff --git a/cpp_source/als/wrapper.cpp b/cpp_source/als/wrapper.cpp index f4e4853..c170d36 100644 --- a/cpp_source/als/wrapper.cpp +++ b/cpp_source/als/wrapper.cpp @@ -1,21 +1,17 @@ #include "IALSLearningConfig.hpp" #include "IALSTrainer.hpp" -#include "pybind11/cast.h" #include #include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include -namespace py = pybind11; using namespace irspack::ials; -using std::vector; +using namespace nanobind; -PYBIND11_MODULE(_ials, m) { +NB_MODULE(_ials_core, m) { std::stringstream doc_stream; doc_stream << "irspack's core module for \"IALSRecommender\"." << std::endl << "Built to use" << std::endl @@ -23,42 +19,37 @@ PYBIND11_MODULE(_ials, m) { m.doc() = doc_stream.str(); - py::enum_(m, "LossType") - .value("ORIGINAL", LossType::ORIGINAL) - .value("IALSPP", LossType::IALSPP) - .export_values(); + // nanobind::enum_(m, "LossType") + // .value("ORIGINAL", LossType::ORIGINAL) + // .value("IALSPP", LossType::IALSPP) + // .export_values(); - py::enum_(m, "SolverType") - .value("CHOLESKY", SolverType::Cholesky) - .value("CG", SolverType::CG) - .value("IALSPP", SolverType::IALSPP) - .export_values(); + // nanobind::enum_(m, "SolverType") + // .value("CHOLESKY", SolverType::Cholesky) + // .value("CG", SolverType::CG) + // .value("IALSPP", SolverType::IALSPP) + // .export_values(); auto model_config = - py::class_(m, "IALSModelConfig") - .def(py::init()) - .def(py::pickle( - [](const IALSModelConfig &config) { - return py::make_tuple(config.K, config.alpha0, config.reg, - config.nu, config.init_stdev, - config.random_seed, config.loss_type); - }, - [](py::tuple t) { - if (t.size() != 7) - throw std::runtime_error("invalid state"); - - size_t K = t[0].cast(); - Real alpha0 = t[1].cast(); - Real reg = t[2].cast(); - Real nu = t[3].cast(); - Real init_stdev = t[4].cast(); - int random_seed = t[5].cast(); - LossType loss_type = t[6].cast(); - return IALSModelConfig(K, alpha0, reg, nu, init_stdev, - random_seed, loss_type); - })); - py::class_(m, "IALSModelConfigBuilder") - .def(py::init<>()) + nanobind::class_(m, "IALSModelConfig") + .def(nanobind::init()) + .def("__getstate__", + [](const IALSModelConfig &config) { + return nanobind::make_tuple( + config.K, config.alpha0, config.reg, config.nu, + config.init_stdev, config.random_seed, config.loss_type); + }) + .def("__setstate__", + [](IALSModelConfig &ials_model_config, + const std::tuple &state) { + new (&ials_model_config) IALSModelConfig( + std::get<0>(state), std::get<1>(state), std::get<2>(state), + std::get<3>(state), std::get<4>(state), std::get<5>(state), + std::get<6>(state)); + }); + nanobind::class_(m, "IALSModelConfigBuilder") + .def(nanobind::init<>()) .def("build", &IALSModelConfig::Builder::build) .def("set_K", &IALSModelConfig::Builder::set_K) .def("set_alpha0", &IALSModelConfig::Builder::set_alpha0) @@ -69,30 +60,28 @@ PYBIND11_MODULE(_ials, m) { .def("set_loss_type", &IALSModelConfig::Builder::set_loss_type); auto solver_config = - py::class_(m, "IALSSolverConfig") - .def(py::init()) - .def(py::pickle( - [](const SolverConfig &config) { - return py::make_tuple( + nanobind::class_(m, "IALSSolverConfig") + .def(nanobind::init()) + .def( + "__getstate__", [](const SolverConfig &config) { + return std::make_tuple( config.n_threads, config.solver_type, config.max_cg_steps, config.ialspp_subspace_dimension, config.ialspp_iteration); - }, - [](py::tuple t) { - if (t.size() != 5) - throw std::runtime_error("invalid state"); - - size_t n_threads = t[0].cast(); - SolverType solver_type = t[1].cast(); - size_t max_cg_steps = t[2].cast(); - size_t ialspp_subspace_dimension = t[3].cast(); - size_t ialspp_iteration = t[4].cast(); - return SolverConfig(n_threads, solver_type, max_cg_steps, - ialspp_subspace_dimension, - ialspp_iteration); - })); + } + ) + .def( + "__setstate__", [](SolverConfig &config, const std::tuple &state) { + new (&config) SolverConfig( + std::get<0>(state), + std::get<1>(state), + std::get<2>(state), + std::get<3>(state), + std::get<4>(state)); + } + ); - py::class_(m, "IALSSolverConfigBuilder") - .def(py::init<>()) + nanobind::class_(m, "IALSSolverConfigBuilder") + .def(nanobind::init<>()) .def("build", &SolverConfig::Builder::build) .def("set_n_threads", &SolverConfig::Builder::set_n_threads) .def("set_solver_type", &SolverConfig::Builder::set_solver_type) @@ -102,25 +91,23 @@ PYBIND11_MODULE(_ials, m) { .def("set_ialspp_iteration", &SolverConfig::Builder::set_ialspp_iteration); - py::class_(m, "IALSTrainer") - .def(py::init()) + nanobind::class_(m, "IALSTrainer") + .def(nanobind::init()) .def("step", &IALSTrainer::step) .def("user_scores", &IALSTrainer::user_scores) .def("transform_user", &IALSTrainer::transform_user) .def("transform_item", &IALSTrainer::transform_item) .def("compute_loss", &IALSTrainer::compute_loss) - .def_readwrite("user", &IALSTrainer::user) - .def_readwrite("item", &IALSTrainer::item) - .def(py::pickle( - [](const IALSTrainer &trainer) { - return py::make_tuple(trainer.config_, trainer.user, trainer.item); - }, - [](py::tuple t) { - if (t.size() != 3) - throw std::runtime_error("Invalid state!"); - IALSTrainer trainer(t[0].cast(), - t[1].cast(), - t[2].cast()); - return trainer; - })); + .def_rw("user", &IALSTrainer::user) + .def_rw("item", &IALSTrainer::item) + .def("__getstate__", [](const IALSTrainer & trainer) { + return std::make_tuple(trainer.config_, trainer.user, trainer.item); + }) + .def("__setstate__", [](IALSTrainer & trainer, const std::tuple & state) { + new (&trainer) IALSTrainer( + std::get<0>(state), std::get<1>(state), + std::get<2>(state) + + ); + }); } diff --git a/cpp_source/evaluator.cpp b/cpp_source/evaluator.cpp index 32e5b0d..541d806 100644 --- a/cpp_source/evaluator.cpp +++ b/cpp_source/evaluator.cpp @@ -1,20 +1,20 @@ #include #include +#include #include #include #include #include -#include #include -#include #include +#include #include -#include #include -#include -#include -#include +#include +#include +#include +#include #include "argcheck.hpp" @@ -416,48 +416,49 @@ Metrics evaluate_list_vs_list(const vector> &recommendation, } // namespace irspack -namespace py = pybind11; using namespace irspack; using namespace irspack::evaluation; using std::vector; -PYBIND11_MODULE(_core, m) { - py::class_(m, "Metrics") - .def(py::init()) +NB_MODULE(_core_evaluator, m) { + nanobind::class_(m, "Metrics") + .def(nanobind::init()) .def("merge", [](Metrics &this_, const Metrics &other) { this_.merge(other); }) .def("as_dict", &Metrics::as_dict); - py::class_(m, "EvaluatorCore") - .def(py::init> &>(), - py::arg("grount_truth"), py::arg("recommendable")) + nanobind::class_(m, "EvaluatorCore") + .def(nanobind::init> &>(), + nanobind::arg("grount_truth"), nanobind::arg("recommendable")) .def("get_metrics_f64", static_cast> &, size_t, size_t, size_t, bool)>(&EvaluatorCore::get_metrics), - py::arg("score_array"), py::arg("cutoff"), py::arg("offset"), - py::arg("n_threads"), py::arg("recall_with_cutoff") = false) + nanobind::arg("score_array"), nanobind::arg("cutoff"), nanobind::arg("offset"), + nanobind::arg("n_threads"), nanobind::arg("recall_with_cutoff") = false) .def("get_metrics_f32", static_cast> &, size_t, size_t, size_t, bool)>(&EvaluatorCore::get_metrics), - py::arg("score_array"), py::arg("cutoff"), py::arg("offset"), - py::arg("n_threads"), py::arg("recall_with_cutoff") = false) + nanobind::arg("score_array"), nanobind::arg("cutoff"), nanobind::arg("offset"), + nanobind::arg("n_threads"), nanobind::arg("recall_with_cutoff") = false) .def("get_ground_truth", &EvaluatorCore::get_ground_truth) .def("cache_X_as_set", &EvaluatorCore::cache_X_map) - .def(py::pickle( - [](const EvaluatorCore &evaluator) { - return py::make_tuple(evaluator.get_ground_truth(), - evaluator.get_recommendable_items()); - }, - [](py::tuple t) { - if (t.size() != 2) - throw std::runtime_error("invalid state"); - return EvaluatorCore(t[0].cast(), - t[1].cast>>()); - })); - + .def("__getstate__", [](const EvaluatorCore & evaluator) { + return std::make_tuple( + evaluator.get_ground_truth(), evaluator.get_recommendable_items() + ); + }) + .def("__setstate__", [](EvaluatorCore & evaluator, const std::tuple>>& state) { + new (&evaluator) EvaluatorCore( + std::get<0>(state), + std::get<1>(state) + + + ); + } + ); m.def("evaluate_list_vs_list", &evaluate_list_vs_list, - py::arg("recomemndations"), py::arg("grount_truths"), - py::arg("n_items"), py::arg("n_threads")); + nanobind::arg("recomemndations"), nanobind::arg("grount_truths"), + nanobind::arg("n_items"), nanobind::arg("n_threads")); } diff --git a/cpp_source/knn/knn.hpp b/cpp_source/knn/knn.hpp index fc3c8d2..32f4d5f 100644 --- a/cpp_source/knn/knn.hpp +++ b/cpp_source/knn/knn.hpp @@ -4,7 +4,6 @@ #include #include #include -#include #include #include #include diff --git a/cpp_source/knn/wrapper.cpp b/cpp_source/knn/wrapper.cpp index 7513b7e..8cc0d40 100644 --- a/cpp_source/knn/wrapper.cpp +++ b/cpp_source/knn/wrapper.cpp @@ -2,62 +2,62 @@ #include "similarities.hpp" #include #include -#include -#include -#include +#include +#include +#include +#include #include -namespace py = pybind11; using Real = double; -PYBIND11_MODULE(_knn, m) { - py::class_>(m, "CosineSimilarityComputer") - .def(py::init::CSRMatrix &, +NB_MODULE(_knn, m) { + nanobind::class_>(m, "CosineSimilarityComputer") + .def(nanobind::init::CSRMatrix &, Real, bool, size_t, size_t>(), - py::arg("X"), py::arg("shrinkage"), py::arg("normalize"), - py::arg("n_threads") = 1, py::arg("max_chunk_size") = 128) + nanobind::arg("X"), nanobind::arg("shrinkage"), nanobind::arg("normalize"), + nanobind::arg("n_threads") = 1, nanobind::arg("max_chunk_size") = 128) .def("compute_similarity", &KNN::CosineSimilarityComputer::compute_similarity); - py::class_>(m, + nanobind::class_>(m, "JaccardSimilarityComputer") - .def(py::init::CSRMatrix &, + .def(nanobind::init::CSRMatrix &, Real, size_t, size_t>(), - py::arg("X"), py::arg("shrinkage"), py::arg("n_threads") = 1, - py::arg("max_chunk_size") = 128) + nanobind::arg("X"), nanobind::arg("shrinkage"), nanobind::arg("n_threads") = 1, + nanobind::arg("max_chunk_size") = 128) .def("compute_similarity", &KNN::JaccardSimilarityComputer::compute_similarity); - py::class_>(m, "TverskyIndexComputer") - .def(py::init::CSRMatrix &, Real, + nanobind::class_>(m, "TverskyIndexComputer") + .def(nanobind::init::CSRMatrix &, Real, Real, Real, size_t, size_t>(), - py::arg("X"), py::arg("shrinkage"), py::arg("alpha"), - py::arg("beta"), py::arg("n_threads") = 1, - py::arg("max_chunk_size") = 128) + nanobind::arg("X"), nanobind::arg("shrinkage"), nanobind::arg("alpha"), + nanobind::arg("beta"), nanobind::arg("n_threads") = 1, + nanobind::arg("max_chunk_size") = 128) .def("compute_similarity", &KNN::TverskyIndexComputer::compute_similarity); - py::class_>( + nanobind::class_>( m, "AsymmetricSimilarityComputer") - .def(py::init< + .def(nanobind::init< const KNN::AsymmetricCosineSimilarityComputer::CSRMatrix &, Real, Real, size_t, size_t>(), - py::arg("X"), py::arg("shrinkage"), py::arg("alpha"), - py::arg("n_threads") = 1, py::arg("max_chunk_size") = 128) + nanobind::arg("X"), nanobind::arg("shrinkage"), nanobind::arg("alpha"), + nanobind::arg("n_threads") = 1, nanobind::arg("max_chunk_size") = 128) .def("compute_similarity", &KNN::AsymmetricCosineSimilarityComputer::compute_similarity); - py::class_>(m, "P3alphaComputer") - .def(py::init::CSRMatrix &, Real, size_t, + nanobind::class_>(m, "P3alphaComputer") + .def(nanobind::init::CSRMatrix &, Real, size_t, size_t>(), - py::arg("X"), py::arg("alpha") = 0, py::arg("n_threads") = 1, - py::arg("max_chunk_size") = 128) + nanobind::arg("X"), nanobind::arg("alpha") = 0, nanobind::arg("n_threads") = 1, + nanobind::arg("max_chunk_size") = 128) .def("compute_W", &KNN::P3alphaComputer::compute_W); - py::class_>(m, "RP3betaComputer") - .def(py::init::CSRMatrix &, Real, Real, + nanobind::class_>(m, "RP3betaComputer") + .def(nanobind::init::CSRMatrix &, Real, Real, size_t, size_t>(), - py::arg("X"), py::arg("alpha") = 0, py::arg("beta") = 0, - py::arg("n_threads") = 1, py::arg("max_chunk_size") = 128) + nanobind::arg("X"), nanobind::arg("alpha") = 0, nanobind::arg("beta") = 0, + nanobind::arg("n_threads") = 1, nanobind::arg("max_chunk_size") = 128) .def("compute_W", &KNN::RP3betaComputer::compute_W); } diff --git a/cpp_source/util.cpp b/cpp_source/util.cpp index d134422..5fabe7f 100644 --- a/cpp_source/util.cpp +++ b/cpp_source/util.cpp @@ -1,15 +1,10 @@ #include "util.hpp" -#include "pybind11/cast.h" #include -#include -#include -#include -#include -#include +#include +#include -namespace py = pybind11; using namespace irspack; -PYBIND11_MODULE(_util_cpp, m) { +NB_MODULE(_util_cpp, m) { m.def("remove_diagonal", &sparse_util::remove_diagonal); m.def("sparse_mm_threaded", &sparse_util::parallel_sparse_product); @@ -18,26 +13,26 @@ PYBIND11_MODULE(_util_cpp, m) { m.def("rowwise_train_test_split_by_fixed_n", &sparse_util::SplitFixedN::split); m.def("okapi_BM_25_weight", &sparse_util::okapi_BM_25_weight, - py::arg("X"), py::arg("k1") = 1.2, py::arg("b") = 0.75); - m.def("tf_idf_weight", &sparse_util::tf_idf_weight, py::arg("X"), - py::arg("smooth") = true); + nanobind::arg("X"), nanobind::arg("k1") = 1.2, nanobind::arg("b") = 0.75); + m.def("tf_idf_weight", &sparse_util::tf_idf_weight, nanobind::arg("X"), + nanobind::arg("smooth") = true); m.def("slim_weight_allow_negative", &sparse_util::SLIM, - py::arg("X"), py::arg("n_threads"), py::arg("n_iter"), - py::arg("l2_coeff"), py::arg("l1_coeff"), py::arg("tol"), - py::arg("top_k") = -1); + nanobind::arg("X"), nanobind::arg("n_threads"), nanobind::arg("n_iter"), + nanobind::arg("l2_coeff"), nanobind::arg("l1_coeff"), nanobind::arg("tol"), + nanobind::arg("top_k") = -1); m.def("slim_weight_positive_only", &sparse_util::SLIM, - py::arg("X"), py::arg("n_threads"), py::arg("n_iter"), - py::arg("l2_coeff"), py::arg("l1_coeff"), py::arg("tol"), - py::arg("top_k") = -1); + nanobind::arg("X"), nanobind::arg("n_threads"), nanobind::arg("n_iter"), + nanobind::arg("l2_coeff"), nanobind::arg("l1_coeff"), nanobind::arg("tol"), + nanobind::arg("top_k") = -1); m.def("retrieve_recommend_from_score_f64", - &sparse_util::retrieve_recommend_from_score, py::arg("score"), - py::arg("allowed_indices"), py::arg("cutoff"), - py::arg("n_threads") = 1); + &sparse_util::retrieve_recommend_from_score, nanobind::arg("score"), + nanobind::arg("allowed_indices"), nanobind::arg("cutoff"), + nanobind::arg("n_threads") = 1); m.def("retrieve_recommend_from_score_f32", - &sparse_util::retrieve_recommend_from_score, py::arg("score"), - py::arg("allowed_indices"), py::arg("cutoff"), - py::arg("n_threads") = 1); + &sparse_util::retrieve_recommend_from_score, nanobind::arg("score"), + nanobind::arg("allowed_indices"), nanobind::arg("cutoff"), + nanobind::arg("n_threads") = 1); } diff --git a/ext/nanobind b/ext/nanobind new file mode 160000 index 0000000..fc08a19 --- /dev/null +++ b/ext/nanobind @@ -0,0 +1 @@ +Subproject commit fc08a1935404b0e931a1fc08d8347fca466f5a85 diff --git a/pyproject.toml b/pyproject.toml index 0b20321..b41ce42 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,28 @@ -[build-system] -requires = [ - "setuptools>=42", - "wheel", - "pybind11>=2.8.0", - "requests", - "setuptools_scm[toml]>=6.2", +[project] +name = "irspack" +version = "0.4.0" +description = "Implicit feedback-based recommender systems, packed for practitioners." +authors = [ + { name = "Tomoki Ohtsuki", email = "tomoki.ohtsuki.19937@outlook.jp" }, +] +classifiers = [ + "License :: MIT", ] -build-backend = "setuptools.build_meta" +[tool.scikit-build] +# Protect the configuration against future changes in scikit-build-core +minimum-version = "0.4" + +# Setuptools-style build caching in a local directory +build-dir = "build/{wheel_tag}" + +# Build stable ABI wheels for CPython 3.12+ +wheel.py-api = "cp312" + +[build-system] +requires = ["scikit-build-core >=0.4.3", "nanobind >=1.3.2", "requests", "setuptools_scm[toml]>=6.2", "setuptools>=42", "wheel"] +build-backend = "scikit_build_core.build" + [tool.black] ensure_newline_before_comments = true @@ -21,10 +36,18 @@ use_parentheses = true ensure_newline_before_comments = true force_grid_wrap = 0 include_trailing_comma = true -known_third_party = ["pybind11"] +known_third_party = ["nanobind"] line_length = 88 multi_line_output = 3 use_parentheses = true [tool.pycln] all = true + +[tool.cibuildwheel] +build-verbosity = 1 +skip = ["cp38-*", "pp38-*"] +archs = ["auto64"] + +[tool.cibuildwheel.macos.environment] +MACOSX_DEPLOYMENT_TARGET = "10.14" \ No newline at end of file diff --git a/setup.py b/setup.py index 67179f7..4d81c4e 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,7 @@ import os from pathlib import Path -from typing import Any, List, Tuple +from typing import Any -from pybind11.setup_helpers import Pybind11Extension, build_ext from setuptools import find_packages, setup SETUP_DIRECTORY = Path(__file__).resolve().parent @@ -24,48 +23,6 @@ ) -class get_eigen_include(object): - EIGEN3_URL = "https://gitlab.com/libeigen/eigen/-/archive/3.3.7/eigen-3.3.7.zip" - EIGEN3_DIRNAME = "eigen-3.3.7" - - def __str__(self) -> str: - eigen_include_dir = os.environ.get("EIGEN3_INCLUDE_DIR", None) - - if eigen_include_dir is not None: - return eigen_include_dir - - target_dir = SETUP_DIRECTORY / self.EIGEN3_DIRNAME - if target_dir.exists(): - return target_dir.name - - download_target_dir = SETUP_DIRECTORY / "eigen3.zip" - import zipfile - - import requests - - response = requests.get(self.EIGEN3_URL, stream=True) - with download_target_dir.open("wb") as ofs: - for chunk in response.iter_content(chunk_size=1024): - ofs.write(chunk) - - with zipfile.ZipFile(download_target_dir) as ifs: - ifs.extractall() - - return target_dir.name - - -module_name_and_sources: List[Tuple[str, List[str]]] = [ - ("irspack.evaluation._core", ["cpp_source/evaluator.cpp"]), - ("irspack.recommenders._ials", ["cpp_source/als/wrapper.cpp"]), - ("irspack.recommenders._knn", ["cpp_source/knn/wrapper.cpp"]), - ("irspack.utils._util_cpp", ["cpp_source/util.cpp"]), -] -ext_modules = [ - Pybind11Extension(module_name, sources, include_dirs=[get_eigen_include()]) - for module_name, sources in module_name_and_sources -] - - def local_scheme(version: Any) -> str: return "" @@ -79,13 +36,10 @@ def local_scheme(version: Any) -> str: }, # https://github.com/pypa/setuptools_scm/issues/342 author="Tomoki Ohtsuki", author_email="tomoki.otsuki129@gmail.com", - description="Implicit feedback-based recommender systems, packed for practitioners.", long_description=LONG_DESCRIPTION, long_description_content_type="text/markdown", - ext_modules=ext_modules, install_requires=install_requires, include_package_data=True, - cmdclass={"build_ext": build_ext}, packages=find_packages("src"), python_requires=">=3.7", package_dir={"": "src"}, diff --git a/src/irspack/__init__.py b/src/irspack/__init__.py index 59ccfc4..253ac65 100644 --- a/src/irspack/__init__.py +++ b/src/irspack/__init__.py @@ -1,9 +1,4 @@ -try: - from importlib.metadata import PackageNotFoundError, version -except ImportError: - # Python < 3.8 - from importlib_metadata import PackageNotFoundError # type: ignore - from importlib_metadata import version # type: ignore +from importlib.metadata import PackageNotFoundError, version try: __version__ = version("irspack") diff --git a/src/irspack/evaluation/__init__.py b/src/irspack/evaluation/__init__.py index 08776e2..71c177f 100644 --- a/src/irspack/evaluation/__init__.py +++ b/src/irspack/evaluation/__init__.py @@ -1,4 +1,4 @@ -from ._core import EvaluatorCore, Metrics +from ._core_evaluator import EvaluatorCore, Metrics from .evaluator import ( METRIC_NAMES, Evaluator, diff --git a/src/irspack/evaluation/_core.pyi b/src/irspack/evaluation/_core.pyi deleted file mode 100644 index 659b27e..0000000 --- a/src/irspack/evaluation/_core.pyi +++ /dev/null @@ -1,55 +0,0 @@ -m: int -n: int -import typing - -import numpy -import scipy.sparse -from numpy import float32 - -import irspack.evaluation._core - -_Shape = typing.Tuple[int, ...] - -__all__ = ["EvaluatorCore", "Metrics", "evaluate_list_vs_list"] - -class EvaluatorCore: - def __getstate__(self) -> tuple: ... - def __init__( - self, - grount_truth: scipy.sparse.csr_matrix[numpy.float64], - recommendable: typing.List[typing.List[int]], - ) -> None: ... - def __setstate__(self, arg0: tuple) -> None: ... - def cache_X_as_set(self, arg0: int) -> None: ... - def get_ground_truth(self) -> scipy.sparse.csr_matrix[numpy.float64]: ... - def get_metrics_f32( - self, - score_array: numpy.ndarray[typing.Tuple[int, int], numpy.dtype[numpy.float32]], - cutoff: int, - offset: int, - n_threads: int, - recall_with_cutoff: bool = False, - ) -> Metrics: ... - def get_metrics_f64( - self, - score_array: numpy.ndarray[typing.Tuple[int, int], numpy.dtype[numpy.float64]], - cutoff: int, - offset: int, - n_threads: int, - recall_with_cutoff: bool = False, - ) -> Metrics: ... - pass - -class Metrics: - def __init__(self, arg0: int) -> None: ... - def as_dict(self) -> typing.Dict[str, float]: ... - def merge(self, arg0: Metrics) -> None: ... - pass - -def evaluate_list_vs_list( - recomemndations: typing.List[typing.List[int]], - grount_truths: typing.List[typing.List[int]], - n_items: int, - n_threads: int, -) -> Metrics: - pass diff --git a/src/irspack/evaluation/evaluate_df_to_df.py b/src/irspack/evaluation/evaluate_df_to_df.py index c77530f..cb24550 100644 --- a/src/irspack/evaluation/evaluate_df_to_df.py +++ b/src/irspack/evaluation/evaluate_df_to_df.py @@ -3,7 +3,7 @@ import pandas as pd from .._threading import get_n_threads -from ._core import evaluate_list_vs_list +from ._core_evaluator import evaluate_list_vs_list def evaluate_recommendation_df( diff --git a/src/irspack/evaluation/evaluator.py b/src/irspack/evaluation/evaluator.py index 8b566ff..2fee6aa 100644 --- a/src/irspack/evaluation/evaluator.py +++ b/src/irspack/evaluation/evaluator.py @@ -8,7 +8,7 @@ from .._threading import get_n_threads from ..definitions import DenseScoreArray, InteractionMatrix -from ._core import EvaluatorCore, Metrics +from ._core_evaluator import EvaluatorCore, Metrics if TYPE_CHECKING: from ..recommenders.base import BaseRecommender diff --git a/src/irspack/recommenders/ials.py b/src/irspack/recommenders/ials.py index b7bf851..188d7b2 100644 --- a/src/irspack/recommenders/ials.py +++ b/src/irspack/recommenders/ials.py @@ -23,9 +23,9 @@ ParameterRange, UniformIntegerRange, ) -from ._ials import IALSModelConfigBuilder, IALSSolverConfigBuilder -from ._ials import IALSTrainer as CoreTrainer -from ._ials import LossType, SolverType +from ._ials_core import IALSModelConfigBuilder, IALSSolverConfigBuilder +from ._ials_core import IALSTrainer as CoreTrainer +from ._ials_core import LossType, SolverType from .base import ( BaseRecommenderWithItemEmbedding, BaseRecommenderWithUserEmbedding, From 434da8436dbd26309ebb5d87e44ac4ad76af3eeb Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Thu, 10 Jul 2025 11:51:37 +0900 Subject: [PATCH 09/32] fixing nanobind --- CMakeLists.txt | 21 ++++++++++++--------- cpp_source/als/wrapper.cpp | 23 ++++++++++++----------- cpp_source/evaluator.cpp | 6 ++++++ cpp_source/knn/wrapper.cpp | 4 +--- cpp_source/util.cpp | 5 +++++ 5 files changed, 36 insertions(+), 23 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3e48f12..d42aeae 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,17 +30,20 @@ find_package(Python 3.8 COMPONENTS Interpreter ${DEV_MODULE} REQUIRED) add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/ext/nanobind) nanobind_add_module(_ials_core cpp_source/als/wrapper.cpp) -nanobind_add_module(irspack.recommenders._knn cpp_source/knn/wrapper.cpp) -nanobind_add_module(irspack.utils._util_cpp cpp_source/util.cpp) +nanobind_add_module(_knn cpp_source/knn/wrapper.cpp) +nanobind_add_module(_util_cpp cpp_source/util.cpp) nanobind_add_module(_core_evaluator cpp_source/evaluator.cpp) install(TARGETS _ials_core LIBRARY DESTINATION irspack/recommenders) install(TARGETS _core_evaluator LIBRARY DESTINATION irspack/evaluation) +install(TARGETS _util_cpp LIBRARY DESTINATION irspack/utils) +install(TARGETS _knn LIBRARY DESTINATION irspack/recommenders) -#nanobind_add_stub( -# _ials_core_stub -# MODULE irspack.recommenders._ials_core -# OUTPUT irspack.recommenders._ials_core.pyi -# PYTHON_PATH "./src" -# DEPENDS _ials_core -#) \ No newline at end of file + +nanobind_add_stub( + _ials_core_stub + MODULE irspack.recommenders._ials_core + OUTPUT irspack.recommenders._ials_core.pyi + PYTHON_PATH "./src" + DEPENDS _ials_core +) \ No newline at end of file diff --git a/cpp_source/als/wrapper.cpp b/cpp_source/als/wrapper.cpp index c170d36..5c064f1 100644 --- a/cpp_source/als/wrapper.cpp +++ b/cpp_source/als/wrapper.cpp @@ -2,10 +2,11 @@ #include "IALSTrainer.hpp" #include #include +#include #include #include #include -#include +#include #include using namespace irspack::ials; @@ -17,18 +18,18 @@ NB_MODULE(_ials_core, m) { << "Built to use" << std::endl << "\t" << Eigen::SimdInstructionSetsInUse(); - m.doc() = doc_stream.str(); + // m.doc() = doc_stream.str(); - // nanobind::enum_(m, "LossType") - // .value("ORIGINAL", LossType::ORIGINAL) - // .value("IALSPP", LossType::IALSPP) - // .export_values(); + nanobind::enum_(m, "LossType") + .value("ORIGINAL", LossType::ORIGINAL) + .value("IALSPP", LossType::IALSPP) + .export_values(); - // nanobind::enum_(m, "SolverType") - // .value("CHOLESKY", SolverType::Cholesky) - // .value("CG", SolverType::CG) - // .value("IALSPP", SolverType::IALSPP) - // .export_values(); + nanobind::enum_(m, "SolverType") + .value("CHOLESKY", SolverType::Cholesky) + .value("CG", SolverType::CG) + .value("IALSPP", SolverType::IALSPP) + .export_values(); auto model_config = nanobind::class_(m, "IALSModelConfig") diff --git a/cpp_source/evaluator.cpp b/cpp_source/evaluator.cpp index 541d806..02793b4 100644 --- a/cpp_source/evaluator.cpp +++ b/cpp_source/evaluator.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -15,6 +16,11 @@ #include #include #include +#include +#include +#include +#include + #include "argcheck.hpp" diff --git a/cpp_source/knn/wrapper.cpp b/cpp_source/knn/wrapper.cpp index 8cc0d40..0eca685 100644 --- a/cpp_source/knn/wrapper.cpp +++ b/cpp_source/knn/wrapper.cpp @@ -1,12 +1,10 @@ -#include "knn.hpp" #include "similarities.hpp" #include #include +#include #include #include #include -#include -#include using Real = double; diff --git a/cpp_source/util.cpp b/cpp_source/util.cpp index 5fabe7f..f51be5e 100644 --- a/cpp_source/util.cpp +++ b/cpp_source/util.cpp @@ -2,6 +2,11 @@ #include #include #include +#include +#include +#include +#include + using namespace irspack; NB_MODULE(_util_cpp, m) { From dbe7ca7fb01402d32502c4b4486d94bdb4d85c2a Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Thu, 10 Jul 2025 12:00:49 +0900 Subject: [PATCH 10/32] stub shell scripts --- CMakeLists.txt | 9 - create_pb_stubs.sh | 14 +- py.typed | 0 src/irspack/evaluation/_core_evaluator.pyi | 30 ++++ src/irspack/recommenders/_ials.pyi | 191 --------------------- src/irspack/recommenders/_ials_core.pyi | 103 +++++++++++ src/irspack/recommenders/_knn.pyi | 111 +++--------- src/irspack/utils/_util_cpp.pyi | 98 ++--------- 8 files changed, 169 insertions(+), 387 deletions(-) create mode 100644 py.typed create mode 100644 src/irspack/evaluation/_core_evaluator.pyi delete mode 100644 src/irspack/recommenders/_ials.pyi create mode 100644 src/irspack/recommenders/_ials_core.pyi diff --git a/CMakeLists.txt b/CMakeLists.txt index d42aeae..9de01c6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,12 +38,3 @@ install(TARGETS _ials_core LIBRARY DESTINATION irspack/recommenders) install(TARGETS _core_evaluator LIBRARY DESTINATION irspack/evaluation) install(TARGETS _util_cpp LIBRARY DESTINATION irspack/utils) install(TARGETS _knn LIBRARY DESTINATION irspack/recommenders) - - -nanobind_add_stub( - _ials_core_stub - MODULE irspack.recommenders._ials_core - OUTPUT irspack.recommenders._ials_core.pyi - PYTHON_PATH "./src" - DEPENDS _ials_core -) \ No newline at end of file diff --git a/create_pb_stubs.sh b/create_pb_stubs.sh index 03b5564..252880e 100755 --- a/create_pb_stubs.sh +++ b/create_pb_stubs.sh @@ -1,21 +1,13 @@ #!/bin/bash modules=( \ -"irspack.recommenders._ials" \ -"irspack.evaluation._core" \ +"irspack.recommenders._ials_core" \ +"irspack.evaluation._core_evaluator" \ "irspack.recommenders._knn" \ "irspack.utils._util_cpp" ) for module_name in "${modules[@]}" do echo "Create stub for $module_name" - pybind11-stubgen -o stubs --no-setup-py "$module_name" output_path="src/$(echo "${module_name}" | sed 's/\./\//g').pyi" - input_path="stubs/$(echo "${module_name}" | sed 's/\./\//g')-stubs/__init__.pyi" - rm "${output_path}" - echo 'm: int -n: int -from numpy import float32 -' >> "${output_path}" - cat "${input_path}" >> "${output_path}" - black "${output_path}" + python -m "nanobind.stubgen" -m "$module_name" -o "$output_path" done diff --git a/py.typed b/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/src/irspack/evaluation/_core_evaluator.pyi b/src/irspack/evaluation/_core_evaluator.pyi new file mode 100644 index 0000000..279555a --- /dev/null +++ b/src/irspack/evaluation/_core_evaluator.pyi @@ -0,0 +1,30 @@ +from collections.abc import Sequence +from typing import Annotated + +from numpy.typing import ArrayLike +import scipy.sparse + + +class Metrics: + def __init__(self, arg: int, /) -> None: ... + + def merge(self, arg: Metrics, /) -> None: ... + + def as_dict(self) -> dict[str, float]: ... + +class EvaluatorCore: + def __init__(self, grount_truth: scipy.sparse.csr_matrix[float], recommendable: Sequence[Sequence[int]]) -> None: ... + + def get_metrics_f64(self, score_array: Annotated[ArrayLike, dict(dtype='float64', shape=(None, None))], cutoff: int, offset: int, n_threads: int, recall_with_cutoff: bool = False) -> Metrics: ... + + def get_metrics_f32(self, score_array: Annotated[ArrayLike, dict(dtype='float32', shape=(None, None))], cutoff: int, offset: int, n_threads: int, recall_with_cutoff: bool = False) -> Metrics: ... + + def get_ground_truth(self) -> scipy.sparse.csr_matrix[float]: ... + + def cache_X_as_set(self, arg: int, /) -> None: ... + + def __getstate__(self) -> tuple[scipy.sparse.csr_matrix[float], list[list[int]]]: ... + + def __setstate__(self, arg: tuple[scipy.sparse.csr_matrix[float], Sequence[Sequence[int]]], /) -> None: ... + +def evaluate_list_vs_list(recomemndations: Sequence[Sequence[int]], grount_truths: Sequence[Sequence[int]], n_items: int, n_threads: int) -> Metrics: ... diff --git a/src/irspack/recommenders/_ials.pyi b/src/irspack/recommenders/_ials.pyi deleted file mode 100644 index 83c62f7..0000000 --- a/src/irspack/recommenders/_ials.pyi +++ /dev/null @@ -1,191 +0,0 @@ -m: int -n: int - -"""irspack's core module for "IALSRecommender". -Built to use - SSE, SSE2""" -from __future__ import annotations - -import typing - -import numpy -import scipy.sparse - -import irspack.recommenders._ials - -__all__ = [ - "CG", - "CHOLESKY", - "IALSModelConfig", - "IALSModelConfigBuilder", - "IALSPP", - "IALSSolverConfig", - "IALSSolverConfigBuilder", - "IALSTrainer", - "LossType", - "ORIGINAL", - "SolverType", -] - -class IALSModelConfig: - def __getstate__(self) -> tuple: ... - def __init__( - self, - arg0: int, - arg1: float, - arg2: float, - arg3: float, - arg4: float, - arg5: int, - arg6: LossType, - ) -> None: ... - def __setstate__(self, arg0: tuple) -> None: ... - pass - -class IALSModelConfigBuilder: - def __init__(self) -> None: ... - def build(self) -> IALSModelConfig: ... - def set_K(self, arg0: int) -> IALSModelConfigBuilder: ... - def set_alpha0(self, arg0: float) -> IALSModelConfigBuilder: ... - def set_init_stdev(self, arg0: float) -> IALSModelConfigBuilder: ... - def set_loss_type(self, arg0: LossType) -> IALSModelConfigBuilder: ... - def set_nu(self, arg0: float) -> IALSModelConfigBuilder: ... - def set_random_seed(self, arg0: int) -> IALSModelConfigBuilder: ... - def set_reg(self, arg0: float) -> IALSModelConfigBuilder: ... - pass - -class IALSSolverConfig: - def __getstate__(self) -> tuple: ... - def __init__( - self, arg0: int, arg1: SolverType, arg2: int, arg3: int, arg4: int - ) -> None: ... - def __setstate__(self, arg0: tuple) -> None: ... - pass - -class IALSSolverConfigBuilder: - def __init__(self) -> None: ... - def build(self) -> IALSSolverConfig: ... - def set_ialspp_iteration(self, arg0: int) -> IALSSolverConfigBuilder: ... - def set_ialspp_subspace_dimension(self, arg0: int) -> IALSSolverConfigBuilder: ... - def set_max_cg_steps(self, arg0: int) -> IALSSolverConfigBuilder: ... - def set_n_threads(self, arg0: int) -> IALSSolverConfigBuilder: ... - def set_solver_type(self, arg0: SolverType) -> IALSSolverConfigBuilder: ... - pass - -class IALSTrainer: - def __getstate__(self) -> tuple: ... - def __init__( - self, arg0: IALSModelConfig, arg1: scipy.sparse.csr_matrix[numpy.float32] - ) -> None: ... - def __setstate__(self, arg0: tuple) -> None: ... - def compute_loss(self, arg0: IALSSolverConfig) -> float: ... - def step(self, arg0: IALSSolverConfig) -> None: ... - def transform_item( - self, arg0: scipy.sparse.csr_matrix[numpy.float32], arg1: IALSSolverConfig - ) -> numpy.ndarray[typing.Tuple[int, int], numpy.dtype[numpy.float32]]: ... - def transform_user( - self, arg0: scipy.sparse.csr_matrix[numpy.float32], arg1: IALSSolverConfig - ) -> numpy.ndarray[typing.Tuple[int, int], numpy.dtype[numpy.float32]]: ... - def user_scores( - self, arg0: int, arg1: int, arg2: IALSSolverConfig - ) -> numpy.ndarray[typing.Tuple[int, int], numpy.dtype[numpy.float32]]: ... - @property - def item(self) -> numpy.ndarray[typing.Tuple[int, int], numpy.dtype[numpy.float32]]: - """ - :type: numpy.ndarray[typing.Tuple[int, int], numpy.dtype[numpy.float32]] - """ - - @item.setter - def item( - self, arg0: numpy.ndarray[typing.Tuple[int, int], numpy.dtype[numpy.float32]] - ) -> None: - pass - - @property - def user(self) -> numpy.ndarray[typing.Tuple[int, int], numpy.dtype[numpy.float32]]: - """ - :type: numpy.ndarray[typing.Tuple[int, int], numpy.dtype[numpy.float32]] - """ - - @user.setter - def user( - self, arg0: numpy.ndarray[typing.Tuple[int, int], numpy.dtype[numpy.float32]] - ) -> None: - pass - pass - -class LossType: - """ - Members: - - ORIGINAL - - IALSPP - """ - - def __eq__(self, other: object) -> bool: ... - def __getstate__(self) -> int: ... - def __hash__(self) -> int: ... - def __index__(self) -> int: ... - def __init__(self, value: int) -> None: ... - def __int__(self) -> int: ... - def __ne__(self, other: object) -> bool: ... - def __repr__(self) -> str: ... - def __setstate__(self, state: int) -> None: ... - @property - def name(self) -> str: - """ - :type: str - """ - - @property - def value(self) -> int: - """ - :type: int - """ - IALSPP: irspack.recommenders._ials.LossType # value = - ORIGINAL: irspack.recommenders._ials.LossType # value = - __members__: dict # value = {'ORIGINAL': , 'IALSPP': } - pass - -class SolverType: - """ - Members: - - CHOLESKY - - CG - - IALSPP - """ - - def __eq__(self, other: object) -> bool: ... - def __getstate__(self) -> int: ... - def __hash__(self) -> int: ... - def __index__(self) -> int: ... - def __init__(self, value: int) -> None: ... - def __int__(self) -> int: ... - def __ne__(self, other: object) -> bool: ... - def __repr__(self) -> str: ... - def __setstate__(self, state: int) -> None: ... - @property - def name(self) -> str: - """ - :type: str - """ - - @property - def value(self) -> int: - """ - :type: int - """ - CG: irspack.recommenders._ials.SolverType # value = - CHOLESKY: irspack.recommenders._ials.SolverType # value = - IALSPP: irspack.recommenders._ials.SolverType # value = - __members__: dict # value = {'CHOLESKY': , 'CG': , 'IALSPP': } - pass - -CG: irspack.recommenders._ials.SolverType # value = -CHOLESKY: irspack.recommenders._ials.SolverType # value = -IALSPP: irspack.recommenders._ials.SolverType # value = -ORIGINAL: irspack.recommenders._ials.LossType # value = diff --git a/src/irspack/recommenders/_ials_core.pyi b/src/irspack/recommenders/_ials_core.pyi new file mode 100644 index 0000000..96a87f7 --- /dev/null +++ b/src/irspack/recommenders/_ials_core.pyi @@ -0,0 +1,103 @@ +import enum +from typing import Annotated + +from numpy.typing import ArrayLike +import scipy.sparse + + +class LossType(enum.Enum): + ORIGINAL = 0 + + IALSPP = 1 + +ORIGINAL: LossType = LossType.ORIGINAL + +IALSPP: SolverType = SolverType.IALSPP + +class SolverType(enum.Enum): + CHOLESKY = 0 + + CG = 1 + + IALSPP = 2 + +CHOLESKY: SolverType = SolverType.CHOLESKY + +CG: SolverType = SolverType.CG + +class IALSModelConfig: + def __init__(self, arg0: int, arg1: float, arg2: float, arg3: float, arg4: float, arg5: int, arg6: LossType, /) -> None: ... + + def __getstate__(self) -> tuple: ... + + def __setstate__(self, arg: tuple[int, float, float, float, float, int, LossType], /) -> None: ... + +class IALSModelConfigBuilder: + def __init__(self) -> None: ... + + def build(self) -> IALSModelConfig: ... + + def set_K(self, arg: int, /) -> IALSModelConfigBuilder: ... + + def set_alpha0(self, arg: float, /) -> IALSModelConfigBuilder: ... + + def set_reg(self, arg: float, /) -> IALSModelConfigBuilder: ... + + def set_nu(self, arg: float, /) -> IALSModelConfigBuilder: ... + + def set_init_stdev(self, arg: float, /) -> IALSModelConfigBuilder: ... + + def set_random_seed(self, arg: int, /) -> IALSModelConfigBuilder: ... + + def set_loss_type(self, arg: LossType, /) -> IALSModelConfigBuilder: ... + +class IALSSolverConfig: + def __init__(self, arg0: int, arg1: SolverType, arg2: int, arg3: int, arg4: int, /) -> None: ... + + def __getstate__(self) -> tuple[int, SolverType, int, int, int]: ... + + def __setstate__(self, arg: tuple[int, SolverType, int, int, int], /) -> None: ... + +class IALSSolverConfigBuilder: + def __init__(self) -> None: ... + + def build(self) -> IALSSolverConfig: ... + + def set_n_threads(self, arg: int, /) -> IALSSolverConfigBuilder: ... + + def set_solver_type(self, arg: SolverType, /) -> IALSSolverConfigBuilder: ... + + def set_max_cg_steps(self, arg: int, /) -> IALSSolverConfigBuilder: ... + + def set_ialspp_subspace_dimension(self, arg: int, /) -> IALSSolverConfigBuilder: ... + + def set_ialspp_iteration(self, arg: int, /) -> IALSSolverConfigBuilder: ... + +class IALSTrainer: + def __init__(self, arg0: IALSModelConfig, arg1: scipy.sparse.csr_matrix[float], /) -> None: ... + + def step(self, arg: IALSSolverConfig, /) -> None: ... + + def user_scores(self, arg0: int, arg1: int, arg2: IALSSolverConfig, /) -> Annotated[ArrayLike, dict(dtype='float32', shape=(None, None), order='C')]: ... + + def transform_user(self, arg0: scipy.sparse.csr_matrix[float], arg1: IALSSolverConfig, /) -> Annotated[ArrayLike, dict(dtype='float32', shape=(None, None), order='C')]: ... + + def transform_item(self, arg0: scipy.sparse.csr_matrix[float], arg1: IALSSolverConfig, /) -> Annotated[ArrayLike, dict(dtype='float32', shape=(None, None), order='C')]: ... + + def compute_loss(self, arg: IALSSolverConfig, /) -> float: ... + + @property + def user(self) -> Annotated[ArrayLike, dict(dtype='float32', shape=(None, None), order='C')]: ... + + @user.setter + def user(self, arg: Annotated[ArrayLike, dict(dtype='float32', shape=(None, None), order='C')], /) -> None: ... + + @property + def item(self) -> Annotated[ArrayLike, dict(dtype='float32', shape=(None, None), order='C')]: ... + + @item.setter + def item(self, arg: Annotated[ArrayLike, dict(dtype='float32', shape=(None, None), order='C')], /) -> None: ... + + def __getstate__(self) -> tuple[IALSModelConfig, Annotated[ArrayLike, dict(dtype='float32', shape=(None, None), order='C')], Annotated[ArrayLike, dict(dtype='float32', shape=(None, None), order='C')]]: ... + + def __setstate__(self, arg: tuple[IALSModelConfig, Annotated[ArrayLike, dict(dtype='float32', shape=(None, None), order='C')], Annotated[ArrayLike, dict(dtype='float32', shape=(None, None), order='C')]], /) -> None: ... diff --git a/src/irspack/recommenders/_knn.pyi b/src/irspack/recommenders/_knn.pyi index 8da7123..170b03c 100644 --- a/src/irspack/recommenders/_knn.pyi +++ b/src/irspack/recommenders/_knn.pyi @@ -1,103 +1,32 @@ -m: int -n: int -import typing - -import numpy import scipy.sparse -from numpy import float32 -import irspack.recommenders._knn -_Shape = typing.Tuple[int, ...] +class CosineSimilarityComputer: + def __init__(self, X: scipy.sparse.csr_matrix[float], shrinkage: float, normalize: bool, n_threads: int = 1, max_chunk_size: int = 128) -> None: ... -__all__ = [ - "AsymmetricSimilarityComputer", - "CosineSimilarityComputer", - "JaccardSimilarityComputer", - "P3alphaComputer", - "RP3betaComputer", - "TverskyIndexComputer", -] + def compute_similarity(self, arg0: scipy.sparse.csr_matrix[float], arg1: int, /) -> scipy.sparse.csr_matrix[float]: ... -class AsymmetricSimilarityComputer: - def __init__( - self, - X: scipy.sparse.csr_matrix[numpy.float64], - shrinkage: float, - alpha: float, - n_threads: int = 1, - max_chunk_size: int = 128, - ) -> None: ... - def compute_similarity( - self, arg0: scipy.sparse.csr_matrix[numpy.float64], arg1: int - ) -> scipy.sparse.csr_matrix[numpy.float64]: ... - pass +class JaccardSimilarityComputer: + def __init__(self, X: scipy.sparse.csr_matrix[float], shrinkage: float, n_threads: int = 1, max_chunk_size: int = 128) -> None: ... -class CosineSimilarityComputer: - def __init__( - self, - X: scipy.sparse.csr_matrix[numpy.float64], - shrinkage: float, - normalize: bool, - n_threads: int = 1, - max_chunk_size: int = 128, - ) -> None: ... - def compute_similarity( - self, arg0: scipy.sparse.csr_matrix[numpy.float64], arg1: int - ) -> scipy.sparse.csr_matrix[numpy.float64]: ... - pass + def compute_similarity(self, arg0: scipy.sparse.csr_matrix[float], arg1: int, /) -> scipy.sparse.csr_matrix[float]: ... -class JaccardSimilarityComputer: - def __init__( - self, - X: scipy.sparse.csr_matrix[numpy.float64], - shrinkage: float, - n_threads: int = 1, - max_chunk_size: int = 128, - ) -> None: ... - def compute_similarity( - self, arg0: scipy.sparse.csr_matrix[numpy.float64], arg1: int - ) -> scipy.sparse.csr_matrix[numpy.float64]: ... - pass +class TverskyIndexComputer: + def __init__(self, X: scipy.sparse.csr_matrix[float], shrinkage: float, alpha: float, beta: float, n_threads: int = 1, max_chunk_size: int = 128) -> None: ... + + def compute_similarity(self, arg0: scipy.sparse.csr_matrix[float], arg1: int, /) -> scipy.sparse.csr_matrix[float]: ... + +class AsymmetricSimilarityComputer: + def __init__(self, X: scipy.sparse.csr_matrix[float], shrinkage: float, alpha: float, n_threads: int = 1, max_chunk_size: int = 128) -> None: ... + + def compute_similarity(self, arg0: scipy.sparse.csr_matrix[float], arg1: int, /) -> scipy.sparse.csr_matrix[float]: ... class P3alphaComputer: - def __init__( - self, - X: scipy.sparse.csr_matrix[numpy.float64], - alpha: float = 0, - n_threads: int = 1, - max_chunk_size: int = 128, - ) -> None: ... - def compute_W( - self, arg0: scipy.sparse.csr_matrix[numpy.float64], arg1: int - ) -> scipy.sparse.csc_matrix[numpy.float64]: ... - pass + def __init__(self, X: scipy.sparse.csr_matrix[float], alpha: float = 0, n_threads: int = 1, max_chunk_size: int = 128) -> None: ... + + def compute_W(self, arg0: scipy.sparse.csr_matrix[float], arg1: int, /) -> scipy.sparse.csc_matrix[float]: ... class RP3betaComputer: - def __init__( - self, - X: scipy.sparse.csr_matrix[numpy.float64], - alpha: float = 0, - beta: float = 0, - n_threads: int = 1, - max_chunk_size: int = 128, - ) -> None: ... - def compute_W( - self, arg0: scipy.sparse.csr_matrix[numpy.float64], arg1: int - ) -> scipy.sparse.csc_matrix[numpy.float64]: ... - pass + def __init__(self, X: scipy.sparse.csr_matrix[float], alpha: float = 0, beta: float = 0, n_threads: int = 1, max_chunk_size: int = 128) -> None: ... -class TverskyIndexComputer: - def __init__( - self, - X: scipy.sparse.csr_matrix[numpy.float64], - shrinkage: float, - alpha: float, - beta: float, - n_threads: int = 1, - max_chunk_size: int = 128, - ) -> None: ... - def compute_similarity( - self, arg0: scipy.sparse.csr_matrix[numpy.float64], arg1: int - ) -> scipy.sparse.csr_matrix[numpy.float64]: ... - pass + def compute_W(self, arg0: scipy.sparse.csr_matrix[float], arg1: int, /) -> scipy.sparse.csc_matrix[float]: ... diff --git a/src/irspack/utils/_util_cpp.pyi b/src/irspack/utils/_util_cpp.pyi index bfa8425..895a130 100644 --- a/src/irspack/utils/_util_cpp.pyi +++ b/src/irspack/utils/_util_cpp.pyi @@ -1,98 +1,26 @@ -m: int -n: int -import typing +from collections.abc import Sequence +from typing import Annotated -import numpy +from numpy.typing import ArrayLike import scipy.sparse -from numpy import float32 -import irspack.utils._util_cpp -_Shape = typing.Tuple[int, ...] +def remove_diagonal(arg: scipy.sparse.csr_matrix[float], /) -> scipy.sparse.csr_matrix[float]: ... -__all__ = [ - "okapi_BM_25_weight", - "remove_diagonal", - "retrieve_recommend_from_score_f32", - "retrieve_recommend_from_score_f64", - "rowwise_train_test_split_by_fixed_n", - "rowwise_train_test_split_by_ratio", - "slim_weight_allow_negative", - "slim_weight_positive_only", - "sparse_mm_threaded", - "tf_idf_weight", -] +def sparse_mm_threaded(arg0: scipy.sparse.csr_matrix[float], arg1: scipy.sparse.csc_matrix[float], arg2: int, /) -> Annotated[ArrayLike, dict(dtype='float64', shape=(None, None), order='C')]: ... -def okapi_BM_25_weight( - X: scipy.sparse.csr_matrix[numpy.float64], k1: float = 1.2, b: float = 0.75 -) -> scipy.sparse.csr_matrix[numpy.float64]: - pass +def rowwise_train_test_split_by_ratio(arg0: scipy.sparse.csr_matrix[float], arg1: int, arg2: float, arg3: bool, /) -> tuple[scipy.sparse.csr_matrix[float], scipy.sparse.csr_matrix[float]]: ... -def remove_diagonal( - arg0: scipy.sparse.csr_matrix[numpy.float64], -) -> scipy.sparse.csr_matrix[numpy.float64]: - pass +def rowwise_train_test_split_by_fixed_n(arg0: scipy.sparse.csr_matrix[float], arg1: int, arg2: int, /) -> tuple[scipy.sparse.csr_matrix[float], scipy.sparse.csr_matrix[float]]: ... -def retrieve_recommend_from_score_f32( - score: numpy.ndarray[typing.Tuple[int, int], numpy.dtype[numpy.float32]], - allowed_indices: typing.List[typing.List[int]], - cutoff: int, - n_threads: int = 1, -) -> typing.List[typing.List[typing.Tuple[int, float]]]: - pass +def okapi_BM_25_weight(X: scipy.sparse.csr_matrix[float], k1: float = 1.2, b: float = 0.75) -> scipy.sparse.csr_matrix[float]: ... -def retrieve_recommend_from_score_f64( - score: numpy.ndarray[typing.Tuple[int, int], numpy.dtype[numpy.float64]], - allowed_indices: typing.List[typing.List[int]], - cutoff: int, - n_threads: int = 1, -) -> typing.List[typing.List[typing.Tuple[int, float]]]: - pass +def tf_idf_weight(X: scipy.sparse.csr_matrix[float], smooth: bool = True) -> scipy.sparse.csr_matrix[float]: ... -def rowwise_train_test_split_by_fixed_n( - arg0: scipy.sparse.csr_matrix[numpy.float64], arg1: int, arg2: int -) -> typing.Tuple[ - scipy.sparse.csr_matrix[numpy.float64], scipy.sparse.csr_matrix[numpy.float64] -]: - pass +def slim_weight_allow_negative(X: scipy.sparse.csr_matrix[float], n_threads: int, n_iter: int, l2_coeff: float, l1_coeff: float, tol: float, top_k: int = -1) -> scipy.sparse.csc_matrix[float]: ... -def rowwise_train_test_split_by_ratio( - arg0: scipy.sparse.csr_matrix[numpy.float64], arg1: int, arg2: float, arg3: bool -) -> typing.Tuple[ - scipy.sparse.csr_matrix[numpy.float64], scipy.sparse.csr_matrix[numpy.float64] -]: - pass +def slim_weight_positive_only(X: scipy.sparse.csr_matrix[float], n_threads: int, n_iter: int, l2_coeff: float, l1_coeff: float, tol: float, top_k: int = -1) -> scipy.sparse.csc_matrix[float]: ... -def slim_weight_allow_negative( - X: scipy.sparse.csr_matrix[numpy.float32], - n_threads: int, - n_iter: int, - l2_coeff: float, - l1_coeff: float, - tol: float, - top_k: int = -1, -) -> scipy.sparse.csc_matrix[numpy.float32]: - pass +def retrieve_recommend_from_score_f64(score: Annotated[ArrayLike, dict(dtype='float64', shape=(None, None), order='C')], allowed_indices: Sequence[Sequence[int]], cutoff: int, n_threads: int = 1) -> list[list[tuple[int, float]]]: ... -def slim_weight_positive_only( - X: scipy.sparse.csr_matrix[numpy.float32], - n_threads: int, - n_iter: int, - l2_coeff: float, - l1_coeff: float, - tol: float, - top_k: int = -1, -) -> scipy.sparse.csc_matrix[numpy.float32]: - pass - -def sparse_mm_threaded( - arg0: scipy.sparse.csr_matrix[numpy.float64], - arg1: scipy.sparse.csc_matrix[numpy.float64], - arg2: int, -) -> numpy.ndarray[typing.Tuple[int, int], numpy.dtype[numpy.float64]]: - pass - -def tf_idf_weight( - X: scipy.sparse.csr_matrix[numpy.float64], smooth: bool = True -) -> scipy.sparse.csr_matrix[numpy.float64]: - pass +def retrieve_recommend_from_score_f32(score: Annotated[ArrayLike, dict(dtype='float32', shape=(None, None), order='C')], allowed_indices: Sequence[Sequence[int]], cutoff: int, n_threads: int = 1) -> list[list[tuple[int, float]]]: ... From 409c84b230077a1116d4a83744057462d3aac962 Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Thu, 10 Jul 2025 12:26:35 +0900 Subject: [PATCH 11/32] build system --- .github/workflows/pre-commit.yml | 2 +- .github/workflows/run-test.yml | 2 +- .github/workflows/wheels.yml | 6 +++++- CMakeLists.txt | 10 +++++++++- pyproject.toml | 22 ++++++++++++++-------- requirements.txt | 9 --------- 6 files changed, 30 insertions(+), 21 deletions(-) delete mode 100644 requirements.txt diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 74af5dd..7fd4104 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -8,6 +8,6 @@ jobs: env: SKIP: no-commit-to-branch steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v3 - uses: pre-commit/action@v3.0.0 diff --git a/.github/workflows/run-test.yml b/.github/workflows/run-test.yml index 2567747..3e342bc 100644 --- a/.github/workflows/run-test.yml +++ b/.github/workflows/run-test.yml @@ -8,6 +8,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + submodule: recursive fetch-depth: 0 - name: Setup Python uses: actions/setup-python@v5 @@ -17,7 +18,6 @@ jobs: run: | pip install --upgrade pip setuptools wheel sudo apt-get install lcov - pip install pybind11 numpy FLAGS="-fprofile-arcs -ftest-coverage" export CFLAGS="$FLAGS" export CXXFLAGS="$FLAGS" diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index b734726..dbbf90a 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -11,8 +11,9 @@ jobs: name: Build source distribution runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: + submodule: recursive fetch-depth: 0 - uses: actions/setup-python@v3 name: Install Python @@ -51,6 +52,9 @@ jobs: cflags: '' steps: - uses: actions/checkout@v4 + with: + submodule: recursive + fetch-depth: 0 - name: Build wheels uses: pypa/cibuildwheel@v3.0.1 env: diff --git a/CMakeLists.txt b/CMakeLists.txt index 9de01c6..a6481c2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,7 @@ cmake_minimum_required(VERSION 3.15...3.27) project(irspack VERSION 0.1.0) +include(FetchContent) + if (CMAKE_VERSION VERSION_LESS 3.18) set(DEV_MODULE Development) @@ -16,9 +18,15 @@ if (NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo") endif() +if (NOT EXISTS "${CMAKE_CURRENT_BINARY_DIR}/eigen-3.4.0") + file(DOWNLOAD https://gitlab.com/libeigen/eigen/-/archive/3.4.0/eigen-3.4.0.zip "${CMAKE_CURRENT_BINARY_DIR}/eigen-3.4.0.zip") + file(ARCHIVE_EXTRACT INPUT "${CMAKE_CURRENT_BINARY_DIR}/eigen-3.4.0.zip") +endif() + find_package(Threads REQUIRED) -include_directories(eigen-3.4.0 cpp_source) + +include_directories("${CMAKE_BINARY_DIR}/eigen-3.4.0" cpp_source) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CPACK_PROJECT_NAME ${PROJECT_NAME}) set(CPACK_PROJECT_VERSION ${PROJECT_VERSION}) diff --git a/pyproject.toml b/pyproject.toml index b41ce42..eee15aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] +dynamic = ["version"] name = "irspack" -version = "0.4.0" description = "Implicit feedback-based recommender systems, packed for practitioners." authors = [ { name = "Tomoki Ohtsuki", email = "tomoki.ohtsuki.19937@outlook.jp" }, @@ -8,6 +8,17 @@ authors = [ classifiers = [ "License :: MIT", ] +dependencies = [ + "httpx", + "gidgethub[httpx]>4.0.0", + "numpy >= 2.0", + "fastprogress >= 0.2", + "optuna>=2.5.0", + "pandas>=1.0.0", + "scikit-learn>=0.21.0", + "scipy>=1.0", + "colorlog>=4" +] [tool.scikit-build] # Protect the configuration against future changes in scikit-build-core @@ -20,17 +31,12 @@ build-dir = "build/{wheel_tag}" wheel.py-api = "cp312" [build-system] -requires = ["scikit-build-core >=0.4.3", "nanobind >=1.3.2", "requests", "setuptools_scm[toml]>=6.2", "setuptools>=42", "wheel"] +requires = ["scikit-build-core >=0.4.3", "nanobind >=1.3.2", "requests", "setuptools_scm[toml]>=8", "setuptools>=64", "wheel"] build-backend = "scikit_build_core.build" +[tool.setuptools_scm] [tool.black] -ensure_newline_before_comments = true -force_grid_wrap = 0 -include_trailing_comma = true -line_length = 88 -multi_line_output = 3 -use_parentheses = true [tool.isort] ensure_newline_before_comments = true diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 13663fa..0000000 --- a/requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -numpy >= 1.11 -fastprogress >= 0.2 -optuna>=2.5.0 -pandas>=1.0.0 -scikit-learn>=0.21.0 -scipy>=1.0 -colorlog>=4 -pybind11>=2.4 -requests From 8dbd53ca17091d6d808c992186a487639ff90353 Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Thu, 10 Jul 2025 12:52:32 +0900 Subject: [PATCH 12/32] debug statement --- .github/workflows/run-test.yml | 2 ++ .github/workflows/wheels.yml | 1 - CMakeLists.txt | 2 -- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/run-test.yml b/.github/workflows/run-test.yml index 3e342bc..85f78c9 100644 --- a/.github/workflows/run-test.yml +++ b/.github/workflows/run-test.yml @@ -16,6 +16,8 @@ jobs: python-version: '3.13' - name: Build irspack (ubuntu) run: | + ls + ls ext/nanobind pip install --upgrade pip setuptools wheel sudo apt-get install lcov FLAGS="-fprofile-arcs -ftest-coverage" diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index dbbf90a..8e99b9b 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -1,6 +1,5 @@ name: Build on: - push: release: types: - created diff --git a/CMakeLists.txt b/CMakeLists.txt index a6481c2..bd41659 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,5 @@ cmake_minimum_required(VERSION 3.15...3.27) project(irspack VERSION 0.1.0) -include(FetchContent) - if (CMAKE_VERSION VERSION_LESS 3.18) set(DEV_MODULE Development) From 4326ec4ac7e57e94ead0239798630ea745b63928 Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Thu, 10 Jul 2025 13:01:53 +0900 Subject: [PATCH 13/32] use pip nanobind instead --- .gitmodules | 3 --- CMakeLists.txt | 5 ++++- ext/nanobind | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) delete mode 160000 ext/nanobind diff --git a/.gitmodules b/.gitmodules index 539e4f6..e69de29 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "ext/nanobind"] - path = ext/nanobind - url = https://github.com/wjakob/nanobind diff --git a/CMakeLists.txt b/CMakeLists.txt index bd41659..d766590 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,7 +33,10 @@ include(CPack) # Detect the installed nanobind package and import it into CMake find_package(Python 3.8 COMPONENTS Interpreter ${DEV_MODULE} REQUIRED) -add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/ext/nanobind) +execute_process( + COMMAND "${Python_EXECUTABLE}" -m nanobind --cmake_dir + OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE nanobind_ROOT) +find_package(nanobind CONFIG REQUIRED) nanobind_add_module(_ials_core cpp_source/als/wrapper.cpp) nanobind_add_module(_knn cpp_source/knn/wrapper.cpp) diff --git a/ext/nanobind b/ext/nanobind deleted file mode 160000 index fc08a19..0000000 --- a/ext/nanobind +++ /dev/null @@ -1 +0,0 @@ -Subproject commit fc08a1935404b0e931a1fc08d8347fca466f5a85 From 17a1657a4512af54e10da6c9ca1e85a8469b76e0 Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Thu, 10 Jul 2025 13:02:52 +0900 Subject: [PATCH 14/32] run-test.yml --- .github/workflows/run-test.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/run-test.yml b/.github/workflows/run-test.yml index 85f78c9..3e342bc 100644 --- a/.github/workflows/run-test.yml +++ b/.github/workflows/run-test.yml @@ -16,8 +16,6 @@ jobs: python-version: '3.13' - name: Build irspack (ubuntu) run: | - ls - ls ext/nanobind pip install --upgrade pip setuptools wheel sudo apt-get install lcov FLAGS="-fprofile-arcs -ftest-coverage" From 7b05882f5163d0f2404a01738ae602373480b673 Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Thu, 10 Jul 2025 13:06:28 +0900 Subject: [PATCH 15/32] pydantic as dependency --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index eee15aa..5402cc4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,8 @@ dependencies = [ "pandas>=1.0.0", "scikit-learn>=0.21.0", "scipy>=1.0", - "colorlog>=4" + "colorlog>=4", + "pydantic>=2.0" ] [tool.scikit-build] From 73627dd4a5e2c2ffd8df5ba753319fac78f59be2 Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Thu, 10 Jul 2025 13:12:50 +0900 Subject: [PATCH 16/32] pre-commit --- pyproject.toml | 2 +- src/irspack/evaluation/_core_evaluator.pyi | 52 ++++++---- src/irspack/recommenders/_ials_core.pyi | 106 ++++++++++++--------- src/irspack/recommenders/_knn.pyi | 84 ++++++++++++---- src/irspack/utils/_util_cpp.pyi | 73 ++++++++++---- 5 files changed, 215 insertions(+), 102 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5402cc4..bbe40bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,4 +57,4 @@ skip = ["cp38-*", "pp38-*"] archs = ["auto64"] [tool.cibuildwheel.macos.environment] -MACOSX_DEPLOYMENT_TARGET = "10.14" \ No newline at end of file +MACOSX_DEPLOYMENT_TARGET = "10.14" diff --git a/src/irspack/evaluation/_core_evaluator.pyi b/src/irspack/evaluation/_core_evaluator.pyi index 279555a..3887692 100644 --- a/src/irspack/evaluation/_core_evaluator.pyi +++ b/src/irspack/evaluation/_core_evaluator.pyi @@ -1,30 +1,48 @@ from collections.abc import Sequence from typing import Annotated -from numpy.typing import ArrayLike import scipy.sparse - +from numpy.typing import ArrayLike class Metrics: def __init__(self, arg: int, /) -> None: ... - def merge(self, arg: Metrics, /) -> None: ... - def as_dict(self) -> dict[str, float]: ... class EvaluatorCore: - def __init__(self, grount_truth: scipy.sparse.csr_matrix[float], recommendable: Sequence[Sequence[int]]) -> None: ... - - def get_metrics_f64(self, score_array: Annotated[ArrayLike, dict(dtype='float64', shape=(None, None))], cutoff: int, offset: int, n_threads: int, recall_with_cutoff: bool = False) -> Metrics: ... - - def get_metrics_f32(self, score_array: Annotated[ArrayLike, dict(dtype='float32', shape=(None, None))], cutoff: int, offset: int, n_threads: int, recall_with_cutoff: bool = False) -> Metrics: ... - + def __init__( + self, + grount_truth: scipy.sparse.csr_matrix[float], + recommendable: Sequence[Sequence[int]], + ) -> None: ... + def get_metrics_f64( + self, + score_array: Annotated[ArrayLike, dict(dtype="float64", shape=(None, None))], + cutoff: int, + offset: int, + n_threads: int, + recall_with_cutoff: bool = False, + ) -> Metrics: ... + def get_metrics_f32( + self, + score_array: Annotated[ArrayLike, dict(dtype="float32", shape=(None, None))], + cutoff: int, + offset: int, + n_threads: int, + recall_with_cutoff: bool = False, + ) -> Metrics: ... def get_ground_truth(self) -> scipy.sparse.csr_matrix[float]: ... - def cache_X_as_set(self, arg: int, /) -> None: ... - - def __getstate__(self) -> tuple[scipy.sparse.csr_matrix[float], list[list[int]]]: ... - - def __setstate__(self, arg: tuple[scipy.sparse.csr_matrix[float], Sequence[Sequence[int]]], /) -> None: ... - -def evaluate_list_vs_list(recomemndations: Sequence[Sequence[int]], grount_truths: Sequence[Sequence[int]], n_items: int, n_threads: int) -> Metrics: ... + def __getstate__( + self, + ) -> tuple[scipy.sparse.csr_matrix[float], list[list[int]]]: ... + def __setstate__( + self, arg: tuple[scipy.sparse.csr_matrix[float], Sequence[Sequence[int]]], / + ) -> None: ... + +def evaluate_list_vs_list( + recomemndations: Sequence[Sequence[int]], + grount_truths: Sequence[Sequence[int]], + n_items: int, + n_threads: int, +) -> Metrics: ... diff --git a/src/irspack/recommenders/_ials_core.pyi b/src/irspack/recommenders/_ials_core.pyi index 96a87f7..7ba242a 100644 --- a/src/irspack/recommenders/_ials_core.pyi +++ b/src/irspack/recommenders/_ials_core.pyi @@ -1,9 +1,8 @@ import enum from typing import Annotated -from numpy.typing import ArrayLike import scipy.sparse - +from numpy.typing import ArrayLike class LossType(enum.Enum): ORIGINAL = 0 @@ -26,78 +25,97 @@ CHOLESKY: SolverType = SolverType.CHOLESKY CG: SolverType = SolverType.CG class IALSModelConfig: - def __init__(self, arg0: int, arg1: float, arg2: float, arg3: float, arg4: float, arg5: int, arg6: LossType, /) -> None: ... - + def __init__( + self, + arg0: int, + arg1: float, + arg2: float, + arg3: float, + arg4: float, + arg5: int, + arg6: LossType, + /, + ) -> None: ... def __getstate__(self) -> tuple: ... - - def __setstate__(self, arg: tuple[int, float, float, float, float, int, LossType], /) -> None: ... + def __setstate__( + self, arg: tuple[int, float, float, float, float, int, LossType], / + ) -> None: ... class IALSModelConfigBuilder: def __init__(self) -> None: ... - def build(self) -> IALSModelConfig: ... - def set_K(self, arg: int, /) -> IALSModelConfigBuilder: ... - def set_alpha0(self, arg: float, /) -> IALSModelConfigBuilder: ... - def set_reg(self, arg: float, /) -> IALSModelConfigBuilder: ... - def set_nu(self, arg: float, /) -> IALSModelConfigBuilder: ... - def set_init_stdev(self, arg: float, /) -> IALSModelConfigBuilder: ... - def set_random_seed(self, arg: int, /) -> IALSModelConfigBuilder: ... - def set_loss_type(self, arg: LossType, /) -> IALSModelConfigBuilder: ... class IALSSolverConfig: - def __init__(self, arg0: int, arg1: SolverType, arg2: int, arg3: int, arg4: int, /) -> None: ... - + def __init__( + self, arg0: int, arg1: SolverType, arg2: int, arg3: int, arg4: int, / + ) -> None: ... def __getstate__(self) -> tuple[int, SolverType, int, int, int]: ... - def __setstate__(self, arg: tuple[int, SolverType, int, int, int], /) -> None: ... class IALSSolverConfigBuilder: def __init__(self) -> None: ... - def build(self) -> IALSSolverConfig: ... - def set_n_threads(self, arg: int, /) -> IALSSolverConfigBuilder: ... - def set_solver_type(self, arg: SolverType, /) -> IALSSolverConfigBuilder: ... - def set_max_cg_steps(self, arg: int, /) -> IALSSolverConfigBuilder: ... - def set_ialspp_subspace_dimension(self, arg: int, /) -> IALSSolverConfigBuilder: ... - def set_ialspp_iteration(self, arg: int, /) -> IALSSolverConfigBuilder: ... class IALSTrainer: - def __init__(self, arg0: IALSModelConfig, arg1: scipy.sparse.csr_matrix[float], /) -> None: ... - + def __init__( + self, arg0: IALSModelConfig, arg1: scipy.sparse.csr_matrix[float], / + ) -> None: ... def step(self, arg: IALSSolverConfig, /) -> None: ... - - def user_scores(self, arg0: int, arg1: int, arg2: IALSSolverConfig, /) -> Annotated[ArrayLike, dict(dtype='float32', shape=(None, None), order='C')]: ... - - def transform_user(self, arg0: scipy.sparse.csr_matrix[float], arg1: IALSSolverConfig, /) -> Annotated[ArrayLike, dict(dtype='float32', shape=(None, None), order='C')]: ... - - def transform_item(self, arg0: scipy.sparse.csr_matrix[float], arg1: IALSSolverConfig, /) -> Annotated[ArrayLike, dict(dtype='float32', shape=(None, None), order='C')]: ... - + def user_scores( + self, arg0: int, arg1: int, arg2: IALSSolverConfig, / + ) -> Annotated[ArrayLike, dict(dtype="float32", shape=(None, None), order="C")]: ... + def transform_user( + self, arg0: scipy.sparse.csr_matrix[float], arg1: IALSSolverConfig, / + ) -> Annotated[ArrayLike, dict(dtype="float32", shape=(None, None), order="C")]: ... + def transform_item( + self, arg0: scipy.sparse.csr_matrix[float], arg1: IALSSolverConfig, / + ) -> Annotated[ArrayLike, dict(dtype="float32", shape=(None, None), order="C")]: ... def compute_loss(self, arg: IALSSolverConfig, /) -> float: ... - @property - def user(self) -> Annotated[ArrayLike, dict(dtype='float32', shape=(None, None), order='C')]: ... - + def user( + self, + ) -> Annotated[ArrayLike, dict(dtype="float32", shape=(None, None), order="C")]: ... @user.setter - def user(self, arg: Annotated[ArrayLike, dict(dtype='float32', shape=(None, None), order='C')], /) -> None: ... - + def user( + self, + arg: Annotated[ArrayLike, dict(dtype="float32", shape=(None, None), order="C")], + /, + ) -> None: ... @property - def item(self) -> Annotated[ArrayLike, dict(dtype='float32', shape=(None, None), order='C')]: ... - + def item( + self, + ) -> Annotated[ArrayLike, dict(dtype="float32", shape=(None, None), order="C")]: ... @item.setter - def item(self, arg: Annotated[ArrayLike, dict(dtype='float32', shape=(None, None), order='C')], /) -> None: ... - - def __getstate__(self) -> tuple[IALSModelConfig, Annotated[ArrayLike, dict(dtype='float32', shape=(None, None), order='C')], Annotated[ArrayLike, dict(dtype='float32', shape=(None, None), order='C')]]: ... - - def __setstate__(self, arg: tuple[IALSModelConfig, Annotated[ArrayLike, dict(dtype='float32', shape=(None, None), order='C')], Annotated[ArrayLike, dict(dtype='float32', shape=(None, None), order='C')]], /) -> None: ... + def item( + self, + arg: Annotated[ArrayLike, dict(dtype="float32", shape=(None, None), order="C")], + /, + ) -> None: ... + def __getstate__( + self, + ) -> tuple[ + IALSModelConfig, + Annotated[ArrayLike, dict(dtype="float32", shape=(None, None), order="C")], + Annotated[ArrayLike, dict(dtype="float32", shape=(None, None), order="C")], + ]: ... + def __setstate__( + self, + arg: tuple[ + IALSModelConfig, + Annotated[ArrayLike, dict(dtype="float32", shape=(None, None), order="C")], + Annotated[ArrayLike, dict(dtype="float32", shape=(None, None), order="C")], + ], + /, + ) -> None: ... diff --git a/src/irspack/recommenders/_knn.pyi b/src/irspack/recommenders/_knn.pyi index 170b03c..37f0f90 100644 --- a/src/irspack/recommenders/_knn.pyi +++ b/src/irspack/recommenders/_knn.pyi @@ -1,32 +1,78 @@ import scipy.sparse - class CosineSimilarityComputer: - def __init__(self, X: scipy.sparse.csr_matrix[float], shrinkage: float, normalize: bool, n_threads: int = 1, max_chunk_size: int = 128) -> None: ... - - def compute_similarity(self, arg0: scipy.sparse.csr_matrix[float], arg1: int, /) -> scipy.sparse.csr_matrix[float]: ... + def __init__( + self, + X: scipy.sparse.csr_matrix[float], + shrinkage: float, + normalize: bool, + n_threads: int = 1, + max_chunk_size: int = 128, + ) -> None: ... + def compute_similarity( + self, arg0: scipy.sparse.csr_matrix[float], arg1: int, / + ) -> scipy.sparse.csr_matrix[float]: ... class JaccardSimilarityComputer: - def __init__(self, X: scipy.sparse.csr_matrix[float], shrinkage: float, n_threads: int = 1, max_chunk_size: int = 128) -> None: ... - - def compute_similarity(self, arg0: scipy.sparse.csr_matrix[float], arg1: int, /) -> scipy.sparse.csr_matrix[float]: ... + def __init__( + self, + X: scipy.sparse.csr_matrix[float], + shrinkage: float, + n_threads: int = 1, + max_chunk_size: int = 128, + ) -> None: ... + def compute_similarity( + self, arg0: scipy.sparse.csr_matrix[float], arg1: int, / + ) -> scipy.sparse.csr_matrix[float]: ... class TverskyIndexComputer: - def __init__(self, X: scipy.sparse.csr_matrix[float], shrinkage: float, alpha: float, beta: float, n_threads: int = 1, max_chunk_size: int = 128) -> None: ... - - def compute_similarity(self, arg0: scipy.sparse.csr_matrix[float], arg1: int, /) -> scipy.sparse.csr_matrix[float]: ... + def __init__( + self, + X: scipy.sparse.csr_matrix[float], + shrinkage: float, + alpha: float, + beta: float, + n_threads: int = 1, + max_chunk_size: int = 128, + ) -> None: ... + def compute_similarity( + self, arg0: scipy.sparse.csr_matrix[float], arg1: int, / + ) -> scipy.sparse.csr_matrix[float]: ... class AsymmetricSimilarityComputer: - def __init__(self, X: scipy.sparse.csr_matrix[float], shrinkage: float, alpha: float, n_threads: int = 1, max_chunk_size: int = 128) -> None: ... - - def compute_similarity(self, arg0: scipy.sparse.csr_matrix[float], arg1: int, /) -> scipy.sparse.csr_matrix[float]: ... + def __init__( + self, + X: scipy.sparse.csr_matrix[float], + shrinkage: float, + alpha: float, + n_threads: int = 1, + max_chunk_size: int = 128, + ) -> None: ... + def compute_similarity( + self, arg0: scipy.sparse.csr_matrix[float], arg1: int, / + ) -> scipy.sparse.csr_matrix[float]: ... class P3alphaComputer: - def __init__(self, X: scipy.sparse.csr_matrix[float], alpha: float = 0, n_threads: int = 1, max_chunk_size: int = 128) -> None: ... - - def compute_W(self, arg0: scipy.sparse.csr_matrix[float], arg1: int, /) -> scipy.sparse.csc_matrix[float]: ... + def __init__( + self, + X: scipy.sparse.csr_matrix[float], + alpha: float = 0, + n_threads: int = 1, + max_chunk_size: int = 128, + ) -> None: ... + def compute_W( + self, arg0: scipy.sparse.csr_matrix[float], arg1: int, / + ) -> scipy.sparse.csc_matrix[float]: ... class RP3betaComputer: - def __init__(self, X: scipy.sparse.csr_matrix[float], alpha: float = 0, beta: float = 0, n_threads: int = 1, max_chunk_size: int = 128) -> None: ... - - def compute_W(self, arg0: scipy.sparse.csr_matrix[float], arg1: int, /) -> scipy.sparse.csc_matrix[float]: ... + def __init__( + self, + X: scipy.sparse.csr_matrix[float], + alpha: float = 0, + beta: float = 0, + n_threads: int = 1, + max_chunk_size: int = 128, + ) -> None: ... + def compute_W( + self, arg0: scipy.sparse.csr_matrix[float], arg1: int, / + ) -> scipy.sparse.csc_matrix[float]: ... diff --git a/src/irspack/utils/_util_cpp.pyi b/src/irspack/utils/_util_cpp.pyi index 895a130..ad24c0d 100644 --- a/src/irspack/utils/_util_cpp.pyi +++ b/src/irspack/utils/_util_cpp.pyi @@ -1,26 +1,57 @@ from collections.abc import Sequence from typing import Annotated -from numpy.typing import ArrayLike import scipy.sparse +from numpy.typing import ArrayLike - -def remove_diagonal(arg: scipy.sparse.csr_matrix[float], /) -> scipy.sparse.csr_matrix[float]: ... - -def sparse_mm_threaded(arg0: scipy.sparse.csr_matrix[float], arg1: scipy.sparse.csc_matrix[float], arg2: int, /) -> Annotated[ArrayLike, dict(dtype='float64', shape=(None, None), order='C')]: ... - -def rowwise_train_test_split_by_ratio(arg0: scipy.sparse.csr_matrix[float], arg1: int, arg2: float, arg3: bool, /) -> tuple[scipy.sparse.csr_matrix[float], scipy.sparse.csr_matrix[float]]: ... - -def rowwise_train_test_split_by_fixed_n(arg0: scipy.sparse.csr_matrix[float], arg1: int, arg2: int, /) -> tuple[scipy.sparse.csr_matrix[float], scipy.sparse.csr_matrix[float]]: ... - -def okapi_BM_25_weight(X: scipy.sparse.csr_matrix[float], k1: float = 1.2, b: float = 0.75) -> scipy.sparse.csr_matrix[float]: ... - -def tf_idf_weight(X: scipy.sparse.csr_matrix[float], smooth: bool = True) -> scipy.sparse.csr_matrix[float]: ... - -def slim_weight_allow_negative(X: scipy.sparse.csr_matrix[float], n_threads: int, n_iter: int, l2_coeff: float, l1_coeff: float, tol: float, top_k: int = -1) -> scipy.sparse.csc_matrix[float]: ... - -def slim_weight_positive_only(X: scipy.sparse.csr_matrix[float], n_threads: int, n_iter: int, l2_coeff: float, l1_coeff: float, tol: float, top_k: int = -1) -> scipy.sparse.csc_matrix[float]: ... - -def retrieve_recommend_from_score_f64(score: Annotated[ArrayLike, dict(dtype='float64', shape=(None, None), order='C')], allowed_indices: Sequence[Sequence[int]], cutoff: int, n_threads: int = 1) -> list[list[tuple[int, float]]]: ... - -def retrieve_recommend_from_score_f32(score: Annotated[ArrayLike, dict(dtype='float32', shape=(None, None), order='C')], allowed_indices: Sequence[Sequence[int]], cutoff: int, n_threads: int = 1) -> list[list[tuple[int, float]]]: ... +def remove_diagonal( + arg: scipy.sparse.csr_matrix[float], / +) -> scipy.sparse.csr_matrix[float]: ... +def sparse_mm_threaded( + arg0: scipy.sparse.csr_matrix[float], + arg1: scipy.sparse.csc_matrix[float], + arg2: int, + /, +) -> Annotated[ArrayLike, dict(dtype="float64", shape=(None, None), order="C")]: ... +def rowwise_train_test_split_by_ratio( + arg0: scipy.sparse.csr_matrix[float], arg1: int, arg2: float, arg3: bool, / +) -> tuple[scipy.sparse.csr_matrix[float], scipy.sparse.csr_matrix[float]]: ... +def rowwise_train_test_split_by_fixed_n( + arg0: scipy.sparse.csr_matrix[float], arg1: int, arg2: int, / +) -> tuple[scipy.sparse.csr_matrix[float], scipy.sparse.csr_matrix[float]]: ... +def okapi_BM_25_weight( + X: scipy.sparse.csr_matrix[float], k1: float = 1.2, b: float = 0.75 +) -> scipy.sparse.csr_matrix[float]: ... +def tf_idf_weight( + X: scipy.sparse.csr_matrix[float], smooth: bool = True +) -> scipy.sparse.csr_matrix[float]: ... +def slim_weight_allow_negative( + X: scipy.sparse.csr_matrix[float], + n_threads: int, + n_iter: int, + l2_coeff: float, + l1_coeff: float, + tol: float, + top_k: int = -1, +) -> scipy.sparse.csc_matrix[float]: ... +def slim_weight_positive_only( + X: scipy.sparse.csr_matrix[float], + n_threads: int, + n_iter: int, + l2_coeff: float, + l1_coeff: float, + tol: float, + top_k: int = -1, +) -> scipy.sparse.csc_matrix[float]: ... +def retrieve_recommend_from_score_f64( + score: Annotated[ArrayLike, dict(dtype="float64", shape=(None, None), order="C")], + allowed_indices: Sequence[Sequence[int]], + cutoff: int, + n_threads: int = 1, +) -> list[list[tuple[int, float]]]: ... +def retrieve_recommend_from_score_f32( + score: Annotated[ArrayLike, dict(dtype="float32", shape=(None, None), order="C")], + allowed_indices: Sequence[Sequence[int]], + cutoff: int, + n_threads: int = 1, +) -> list[list[tuple[int, float]]]: ... From 1ae709547da82353a9717cdaa6f16b87807a9f88 Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Thu, 10 Jul 2025 13:19:19 +0900 Subject: [PATCH 17/32] debugging build --- .github/workflows/wheels.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 8e99b9b..dbbf90a 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -1,5 +1,6 @@ name: Build on: + push: release: types: - created From c834a09d1f96c8fac1c24758e25b5aa27d002718 Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Thu, 10 Jul 2025 13:24:37 +0900 Subject: [PATCH 18/32] fix sdist --- .github/workflows/wheels.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index dbbf90a..4795b5d 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -12,15 +12,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - with: - submodule: recursive - fetch-depth: 0 - uses: actions/setup-python@v3 name: Install Python with: python-version: '3.13' - name: Build sdist - run: pip install pybind11 setuptools_scm && python setup.py sdist + run: python -m build --sdist - uses: actions/upload-artifact@v4 with: path: dist/*.tar.gz From 33b643931e99cde21c2bf0f412da1f025cf64af9 Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Thu, 10 Jul 2025 13:27:16 +0900 Subject: [PATCH 19/32] setuptools --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 4795b5d..d7bfc78 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -17,7 +17,7 @@ jobs: with: python-version: '3.13' - name: Build sdist - run: python -m build --sdist + run: pip install wheel setuptools && python -m build --sdist - uses: actions/upload-artifact@v4 with: path: dist/*.tar.gz From 8c0443cdd6c5fff8c0f815a25f687c925fe2b65c Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Thu, 10 Jul 2025 13:28:03 +0900 Subject: [PATCH 20/32] pip install build --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index d7bfc78..981e373 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -17,7 +17,7 @@ jobs: with: python-version: '3.13' - name: Build sdist - run: pip install wheel setuptools && python -m build --sdist + run: pip install build && python -m build --sdist - uses: actions/upload-artifact@v4 with: path: dist/*.tar.gz From 72f13a8d23a39d46ce82e2f97b0ba1a96ab2e12b Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Thu, 10 Jul 2025 14:09:21 +0900 Subject: [PATCH 21/32] pyproject --- pyproject.toml | 7 ++++++- setup.py | 46 ---------------------------------------------- 2 files changed, 6 insertions(+), 47 deletions(-) delete mode 100644 setup.py diff --git a/pyproject.toml b/pyproject.toml index bbe40bd..29806bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,4 +1,5 @@ [project] +license = "MIT" dynamic = ["version"] name = "irspack" description = "Implicit feedback-based recommender systems, packed for practitioners." @@ -6,7 +7,8 @@ authors = [ { name = "Tomoki Ohtsuki", email = "tomoki.ohtsuki.19937@outlook.jp" }, ] classifiers = [ - "License :: MIT", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3 :: Only" ] dependencies = [ "httpx", @@ -21,6 +23,9 @@ dependencies = [ "pydantic>=2.0" ] +[project.urls] +Homepage = "https://github.com/tohtsky/irspack" + [tool.scikit-build] # Protect the configuration against future changes in scikit-build-core minimum-version = "0.4" diff --git a/setup.py b/setup.py deleted file mode 100644 index 4d81c4e..0000000 --- a/setup.py +++ /dev/null @@ -1,46 +0,0 @@ -import os -from pathlib import Path -from typing import Any - -from setuptools import find_packages, setup - -SETUP_DIRECTORY = Path(__file__).resolve().parent - -with (SETUP_DIRECTORY / "Readme.md").open() as ifs: - LONG_DESCRIPTION = ifs.read() - -install_requires = ( - [ - "numpy>=1.12.0", - "fastprogress>=0.2", - "optuna>=2.5.0", - "pandas>=1.0.0", - "scipy>=1.0", - "colorlog>=4", - "pydantic>=1.8.2", - "typing_extensions>=3.10", - ], -) - - -def local_scheme(version: Any) -> str: - return "" - - -setup( - name="irspack", - # version=get_version(), - url="https://irspack.readthedocs.io/", - use_scm_version={ - "local_scheme": local_scheme - }, # https://github.com/pypa/setuptools_scm/issues/342 - author="Tomoki Ohtsuki", - author_email="tomoki.otsuki129@gmail.com", - long_description=LONG_DESCRIPTION, - long_description_content_type="text/markdown", - install_requires=install_requires, - include_package_data=True, - packages=find_packages("src"), - python_requires=">=3.7", - package_dir={"": "src"}, -) From 311fb68fa14ab1ac42520d80f85af28049f0f3da Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Thu, 10 Jul 2025 14:13:40 +0900 Subject: [PATCH 22/32] remove classfiers --- pyproject.toml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 29806bc..5b8cf60 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,10 +6,6 @@ description = "Implicit feedback-based recommender systems, packed for practitio authors = [ { name = "Tomoki Ohtsuki", email = "tomoki.ohtsuki.19937@outlook.jp" }, ] -classifiers = [ - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3 :: Only" -] dependencies = [ "httpx", "gidgethub[httpx]>4.0.0", @@ -41,6 +37,7 @@ requires = ["scikit-build-core >=0.4.3", "nanobind >=1.3.2", "requests", "setupt build-backend = "scikit_build_core.build" [tool.setuptools_scm] +version_file = "irspack/_version.py" [tool.black] From 8fbb10b45748ec135d32ed330b7648c4aedf992c Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Thu, 10 Jul 2025 14:36:24 +0900 Subject: [PATCH 23/32] scm configured --- pyproject.toml | 9 ++++++--- src/irspack/_version.py | 21 +++++++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 src/irspack/_version.py diff --git a/pyproject.toml b/pyproject.toml index 5b8cf60..68c6254 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,19 +25,21 @@ Homepage = "https://github.com/tohtsky/irspack" [tool.scikit-build] # Protect the configuration against future changes in scikit-build-core minimum-version = "0.4" - # Setuptools-style build caching in a local directory build-dir = "build/{wheel_tag}" - # Build stable ABI wheels for CPython 3.12+ wheel.py-api = "cp312" +metadata.version.provider = "scikit_build_core.metadata.setuptools_scm" +sdist.include = ["src/irspack/_version.py"] + [build-system] requires = ["scikit-build-core >=0.4.3", "nanobind >=1.3.2", "requests", "setuptools_scm[toml]>=8", "setuptools>=64", "wheel"] build-backend = "scikit_build_core.build" [tool.setuptools_scm] -version_file = "irspack/_version.py" +write_to = "src/irspack/_version.py" + [tool.black] @@ -60,3 +62,4 @@ archs = ["auto64"] [tool.cibuildwheel.macos.environment] MACOSX_DEPLOYMENT_TARGET = "10.14" + diff --git a/src/irspack/_version.py b/src/irspack/_version.py new file mode 100644 index 0000000..81d1c3c --- /dev/null +++ b/src/irspack/_version.py @@ -0,0 +1,21 @@ +# file generated by setuptools-scm +# don't change, don't track in version control + +__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"] + +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import Tuple + from typing import Union + + VERSION_TUPLE = Tuple[Union[int, str], ...] +else: + VERSION_TUPLE = object + +version: str +__version__: str +__version_tuple__: VERSION_TUPLE +version_tuple: VERSION_TUPLE + +__version__ = version = '0.4.0.2.dev0+g311fb68.d20250710' +__version_tuple__ = version_tuple = (0, 4, 0, 2, 'dev0', 'g311fb68.d20250710') From f7b7a1807f299c1f773abf46708b1df77252ccdc Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Thu, 10 Jul 2025 14:45:08 +0900 Subject: [PATCH 24/32] remove version file --- CMakeLists.txt | 3 --- src/irspack/_version.py | 21 --------------------- 2 files changed, 24 deletions(-) delete mode 100644 src/irspack/_version.py diff --git a/CMakeLists.txt b/CMakeLists.txt index d766590..7411084 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,9 +8,6 @@ else() endif() - -set(CMAKE_BUILD_TYPE DEBUG) - 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") diff --git a/src/irspack/_version.py b/src/irspack/_version.py deleted file mode 100644 index 81d1c3c..0000000 --- a/src/irspack/_version.py +++ /dev/null @@ -1,21 +0,0 @@ -# file generated by setuptools-scm -# don't change, don't track in version control - -__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"] - -TYPE_CHECKING = False -if TYPE_CHECKING: - from typing import Tuple - from typing import Union - - VERSION_TUPLE = Tuple[Union[int, str], ...] -else: - VERSION_TUPLE = object - -version: str -__version__: str -__version_tuple__: VERSION_TUPLE -version_tuple: VERSION_TUPLE - -__version__ = version = '0.4.0.2.dev0+g311fb68.d20250710' -__version_tuple__ = version_tuple = (0, 4, 0, 2, 'dev0', 'g311fb68.d20250710') From b59170a4dc0ca94187c4926353b94f084b055809 Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Thu, 10 Jul 2025 14:45:25 +0900 Subject: [PATCH 25/32] pre-commit --- .github/workflows/wheels.yml | 2 +- pyproject.toml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 981e373..c2002f5 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -17,7 +17,7 @@ jobs: with: python-version: '3.13' - name: Build sdist - run: pip install build && python -m build --sdist + run: pip install build && python -m build --sdist - uses: actions/upload-artifact@v4 with: path: dist/*.tar.gz diff --git a/pyproject.toml b/pyproject.toml index 68c6254..97dbd3d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,4 +62,3 @@ archs = ["auto64"] [tool.cibuildwheel.macos.environment] MACOSX_DEPLOYMENT_TARGET = "10.14" - From 6ffa393b265b9c1d77a72fd1665d20e66bb584bb Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Thu, 10 Jul 2025 15:15:28 +0900 Subject: [PATCH 26/32] remove local version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 97dbd3d..21840c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ build-backend = "scikit_build_core.build" [tool.setuptools_scm] write_to = "src/irspack/_version.py" - +local_scheme = "no-local-version" [tool.black] From 26d7c82ed6d90b97bea7f2cd6b74e9676e533e91 Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Thu, 10 Jul 2025 16:13:47 +0900 Subject: [PATCH 27/32] docstring --- .gitignore | 1 + cpp_source/als/wrapper.cpp | 3 ++- src/irspack/__init__.py | 5 ++--- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 0e75210..2396436 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ docs/build/ docs/source/api_reference/ .cache/ .coverage +src/irspack/_version.py \ No newline at end of file diff --git a/cpp_source/als/wrapper.cpp b/cpp_source/als/wrapper.cpp index 5c064f1..7ea2b40 100644 --- a/cpp_source/als/wrapper.cpp +++ b/cpp_source/als/wrapper.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include using namespace irspack::ials; @@ -18,7 +19,7 @@ NB_MODULE(_ials_core, m) { << "Built to use" << std::endl << "\t" << Eigen::SimdInstructionSetsInUse(); - // m.doc() = doc_stream.str(); + m.doc() = doc_stream.str(); nanobind::enum_(m, "LossType") .value("ORIGINAL", LossType::ORIGINAL) diff --git a/src/irspack/__init__.py b/src/irspack/__init__.py index 253ac65..1bb7be6 100644 --- a/src/irspack/__init__.py +++ b/src/irspack/__init__.py @@ -1,10 +1,9 @@ from importlib.metadata import PackageNotFoundError, version try: - __version__ = version("irspack") + from ._version import __version__ except PackageNotFoundError: # pragma: no cover - # package is not installed - pass # pragma: no cover + ___version__ = "0.0.0" from .definitions import DenseScoreArray, InteractionMatrix, UserIndexArray from .evaluation import * From cb10badee3447969298ccaac5bb8c5e3fb2ac91f Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Thu, 10 Jul 2025 20:57:54 +0900 Subject: [PATCH 28/32] environment-pass option --- pyproject.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 21840c4..a97b3f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,5 +60,10 @@ build-verbosity = 1 skip = ["cp38-*", "pp38-*"] archs = ["auto64"] +[tool.cibuildwheel.linux] +# Export a variable +environment-pass = ["CFLAGS", "CXXFLAGS"] + + [tool.cibuildwheel.macos.environment] MACOSX_DEPLOYMENT_TARGET = "10.14" From 61d8a62e50b58198ac85f64361655549a670010d Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Thu, 10 Jul 2025 21:01:12 +0900 Subject: [PATCH 29/32] update pandas requirements for numpy >= 2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a97b3f2..84f4da3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ dependencies = [ "numpy >= 2.0", "fastprogress >= 0.2", "optuna>=2.5.0", - "pandas>=1.0.0", + "pandas>=2.2.0", "scikit-learn>=0.21.0", "scipy>=1.0", "colorlog>=4", From c06ea75d1b50402f92a2159312ba5e4e641b2ee0 Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Thu, 10 Jul 2025 22:56:16 +0900 Subject: [PATCH 30/32] add cxxflags --- .github/workflows/wheels.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index c2002f5..9b7d158 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -57,6 +57,7 @@ jobs: env: CIBW_PLATFORM: ${{ matrix.platform || 'auto' }} CFLAGS: ${{ matrix.cibw.cflags }} + CXXFLAGS: ${{ matrix.cibw.cflags }} - uses: actions/upload-artifact@v4 with: name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }} From 5b19d41bbabd26d8fd8f64e01a025471009b4e49 Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Thu, 10 Jul 2025 22:56:30 +0900 Subject: [PATCH 31/32] .gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 2396436..fe2ebf8 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,4 @@ docs/build/ docs/source/api_reference/ .cache/ .coverage -src/irspack/_version.py \ No newline at end of file +src/irspack/_version.py From 71261bbcad646c65570deef886618f9f7b683d42 Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Thu, 10 Jul 2025 23:43:25 +0900 Subject: [PATCH 32/32] branches: main --- .github/workflows/wheels.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 9b7d158..6f98af7 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -1,6 +1,8 @@ name: Build on: push: + branches: + - main release: types: - created