From 2b1d3778474fa88a9327583f2e4c03d3419a7798 Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Fri, 11 Jul 2025 13:14:13 +0900 Subject: [PATCH 01/23] nanobind --- .gitignore | 1 + CMakeLists.txt | 46 ++- cpp_source/bind.cpp | 4 +- cpp_source/declare_module.hpp | 453 ++++++++++++++--------------- include/myfm/predictor.hpp | 9 +- include/myfm/variational.hpp | 5 - pyproject.toml | 60 +++- setup.py | 101 ------- src/myfm/gibbs.py | 8 +- src/myfm/utils/encoders/binning.py | 2 +- tests/conftest.py | 4 +- tests/oprobit/test_oprobit_1dim.py | 2 +- tests/utils/test_categorical.py | 8 +- 13 files changed, 315 insertions(+), 388 deletions(-) delete mode 100644 setup.py diff --git a/.gitignore b/.gitignore index 507cca9..6102ae7 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ stubs/* doc/source/api_reference/*.rst .cache +src/myfm/_version.py \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 12171ef..03120a3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,16 +1,42 @@ -set(CMAKE_C_COMPILER gcc) -set(CMAKE_CXX_COMPILER g++) +cmake_minimum_required(VERSION 3.15...3.27) +project(myFM VERSION 0.1.0) -cmake_minimum_required(VERSION 3.0.0) -project(myfm) +if (CMAKE_VERSION VERSION_LESS 3.18) + set(DEV_MODULE Development) +else() + set(DEV_MODULE Development.Module) +endif() -set(CMAKE_BUILD_TYPE Release) -set(CMAKE_CXX_FLAGS " ${CMAKE_CXX_FLAGS_INIT} -std=c++11 -fPIC") -add_subdirectory(pybind11) -include_directories(include eigen-3.3.7) -pybind11_add_module(_myfm src/bind.cpp src/Faddeeva.cc) +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() +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("${CMAKE_BINARY_DIR}/eigen-3.4.0" cpp_source) +include_directories("${CMAKE_SOURCE_DIR}/include" cpp_source) + +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CPACK_PROJECT_NAME ${PROJECT_NAME}) set(CPACK_PROJECT_VERSION ${PROJECT_VERSION}) -set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +include(CPack) + + +# Detect the installed nanobind package and import it into CMake +find_package(Python 3.8 COMPONENTS Interpreter ${DEV_MODULE} REQUIRED) +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(_myfm cpp_source/bind.cpp cpp_source/Faddeeva.cc) + +install(TARGETS _myfm LIBRARY DESTINATION myfm/) \ No newline at end of file diff --git a/cpp_source/bind.cpp b/cpp_source/bind.cpp index 5cda363..b8de5b3 100644 --- a/cpp_source/bind.cpp +++ b/cpp_source/bind.cpp @@ -1,5 +1,7 @@ #include "declare_module.hpp" +#include +#include -PYBIND11_MODULE(_myfm, m) { +NB_MODULE(_myfm, m) { declare_functional(m); } diff --git a/cpp_source/declare_module.hpp b/cpp_source/declare_module.hpp index fca1f05..926d6f8 100644 --- a/cpp_source/declare_module.hpp +++ b/cpp_source/declare_module.hpp @@ -2,29 +2,29 @@ #include #include -#include -#include #include #include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include "myfm/FM.hpp" #include "myfm/FMLearningConfig.hpp" #include "myfm/FMTrainer.hpp" #include "myfm/LearningHistory.hpp" -#include "myfm/OProbitSampler.hpp" #include "myfm/definitions.hpp" #include "myfm/util.hpp" #include "myfm/variational.hpp" using namespace std; -namespace py = pybind11; - template using FMTrainer = myFM::GibbsFMTrainer; template @@ -64,7 +64,7 @@ create_train_vfm( return fm_trainer.learn_with_callback(fm, hyper_param, cb); } -template void declare_functional(py::module &m) { +template void declare_functional(nanobind::module_ &m) { using FMTrainer = FMTrainer; using VFMTrainer = myFM::variational::VariationalFMTrainer; using FM = myFM::FM; @@ -85,16 +85,16 @@ template void declare_functional(py::module &m) { m.doc() = "Backend C++ implementation for myfm."; - py::enum_(m, "TaskType", py::arithmetic()) + nanobind::enum_(m, "TaskType") .value("REGRESSION", TASKTYPE::REGRESSION) .value("CLASSIFICATION", TASKTYPE::CLASSIFICATION) .value("ORDERED", TASKTYPE::ORDERED); - py::class_(m, "FMLearningConfig"); + nanobind::class_(m, "FMLearningConfig"); - py::class_(m, "RelationBlock", - R"delim(The RelationBlock Class.)delim") - .def(py::init, const SparseMatrix &>(), R"delim( + nanobind::class_(m, "RelationBlock", + R"delim(The RelationBlock Class.)delim") + .def(nanobind::init, const SparseMatrix &>(), R"delim( Initializes relation block. Parameters @@ -108,12 +108,12 @@ template void declare_functional(py::module &m) { Note ----- The entries of `original_to_block` must be in the [0, data.shape[0]-1].)delim", - py::arg("original_to_block"), py::arg("data")) - .def_readonly("original_to_block", &RelationBlock::original_to_block) - .def_readonly("data", &RelationBlock::X) - .def_readonly("mapper_size", &RelationBlock::mapper_size) - .def_readonly("block_size", &RelationBlock::block_size) - .def_readonly("feature_size", &RelationBlock::feature_size) + nanobind::arg("original_to_block"), nanobind::arg("data")) + .def_ro("original_to_block", &RelationBlock::original_to_block) + .def_ro("data", &RelationBlock::X) + .def_ro("mapper_size", &RelationBlock::mapper_size) + .def_ro("block_size", &RelationBlock::block_size) + .def_ro("feature_size", &RelationBlock::feature_size) .def("__repr__", [](const RelationBlock &block) { return (myFM::StringBuilder{})( @@ -123,21 +123,19 @@ template void declare_functional(py::module &m) { block.feature_size)(">") .build(); }) - .def(py::pickle( - [](const RelationBlock &block) { - return py::make_tuple(block.original_to_block, block.X); - }, - [](py::tuple t) { - if (t.size() != 2) { - throw std::runtime_error("invalid state for Relationblock."); - } - return new RelationBlock( - t[0].cast>(), - t[1].cast()); - })); + .def("__getstate__", + [](const RelationBlock &block) { + return std::make_tuple(block.original_to_block, block.X); + }) + .def("__setstate__", + [](RelationBlock &block, + const std::tuple, + typename RelationBlock::SparseMatrix> &state) { + new (&block) RelationBlock(std::get<0>(state), std::get<1>(state)); + }); - py::class_(m, "ConfigBuilder") - .def(py::init<>()) + nanobind::class_(m, "ConfigBuilder") + .def(nanobind::init<>()) .def("set_alpha_0", &ConfigBuilder::set_alpha_0) .def("set_beta_0", &ConfigBuilder::set_beta_0) .def("set_gamma_0", &ConfigBuilder::set_gamma_0) @@ -155,11 +153,11 @@ template void declare_functional(py::module &m) { .def("set_cutpoint_groups", &ConfigBuilder::set_cutpoint_groups) .def("build", &ConfigBuilder::build); - py::class_(m, "FM") - .def_readwrite("w0", &FM::w0) - .def_readwrite("w", &FM::w) - .def_readwrite("V", &FM::V) - .def_readwrite("cutpoints", &FM::cutpoints) + nanobind::class_(m, "FM") + .def_rw("w0", &FM::w0) + .def_rw("w", &FM::w) + .def_rw("V", &FM::V) + .def_rw("cutpoints", &FM::cutpoints) .def("predict_score", &FM::predict_score) .def("oprobit_predict_proba", &FM::oprobit_predict_proba) .def("__repr__", @@ -169,36 +167,29 @@ template void declare_functional(py::module &m) { fm.w.rows())(", rank = ")(fm.V.cols())(">") .build(); }) - .def(py::pickle( - [](const FM &fm) { - Real w0 = fm.w0; - Vector w(fm.w); - DenseMatrix V(fm.V); - vector cutpoints(fm.cutpoints); - return py::make_tuple(w0, w, V, cutpoints); - }, - [](py::tuple t) { - if (t.size() == 3) { - /* For the compatibility with earlier versions */ - return new FM(t[0].cast(), t[1].cast(), - t[2].cast()); - } else if (t.size() == 4) { - return new FM(t[0].cast(), t[1].cast(), - t[2].cast(), - t[3].cast>()); - } else { - throw std::runtime_error("invalid state for FM."); - } - })); + .def("__getstate__", + [](const FM &fm) { + Real w0 = fm.w0; + Vector w(fm.w); + DenseMatrix V(fm.V); + vector cutpoints(fm.cutpoints); + return std::make_tuple(w0, w, V, cutpoints); + }) + .def("__setstate__", + [](FM &fm, const std::tuple> &state) { + new (&fm) FM(std::get<0>(state), std::get<1>(state), + std::get<2>(state), std::get<3>(state)); + }); - py::class_(m, "VariationalFM") - .def_readwrite("w0", &VFM::w0) - .def_readwrite("w0_var", &VFM::w0_var) - .def_readwrite("w", &VFM::w) - .def_readwrite("w_var", &VFM::w_var) - .def_readwrite("V", &VFM::V) - .def_readwrite("V_var", &VFM::V_var) - .def_readwrite("cutpoints", &VFM::cutpoints) + nanobind::class_(m, "VariationalFM") + .def_rw("w0", &VFM::w0) + .def_rw("w0_var", &VFM::w0_var) + .def_rw("w", &VFM::w) + .def_rw("w_var", &VFM::w_var) + .def_rw("V", &VFM::V) + .def_rw("V_var", &VFM::V_var) + .def_rw("cutpoints", &VFM::cutpoints) .def("predict_score", &VFM::predict_score) .def("__repr__", [](const VFM &fm) { @@ -207,196 +198,174 @@ template void declare_functional(py::module &m) { fm.w.rows())(", rank = ")(fm.V.cols())(">") .build(); }) - .def(py::pickle( - [](const VFM &fm) { - Real w0 = fm.w0; - Real w0_var = fm.w0_var; - Vector w(fm.w); - Vector w_var(fm.w_var); - DenseMatrix V(fm.V); - DenseMatrix V_var(fm.V_var); - vector cutpoints(fm.cutpoints); - return py::make_tuple(w0, w0_var, w, w_var, V, V_var, cutpoints); - }, - [](py::tuple t) { - if (t.size() == 6) { - /* For the compatibility with earlier versions */ - return new VFM(t[0].cast(), t[1].cast(), - t[2].cast(), t[3].cast(), - t[4].cast(), - t[5].cast()); - } else if (t.size() == 7) { - return new VFM(t[0].cast(), t[1].cast(), - t[2].cast(), t[3].cast(), - t[4].cast(), t[5].cast(), - t[6].cast>()); - } else { - throw std::runtime_error("invalid state for FM."); - } - })); + .def("__getstate__", + [](const VFM &fm) { + Real w0 = fm.w0; + Real w0_var = fm.w0_var; + Vector w(fm.w); + Vector w_var(fm.w_var); + DenseMatrix V(fm.V); + DenseMatrix V_var(fm.V_var); + vector cutpoints(fm.cutpoints); + return std::make_tuple(w0, w0_var, w, w_var, V, V_var, cutpoints); + }) + .def( + "__setstate__", + [](VFM &vfm, const std::tuple> &state) { + new (&vfm) + VFM(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) - py::class_(m, "FMHyperParameters") - .def_readonly("alpha", &Hyper::alpha) - .def_readonly("mu_w", &Hyper::mu_w) - .def_readonly("lambda_w", &Hyper::lambda_w) - .def_readonly("mu_V", &Hyper::mu_V) - .def_readonly("lambda_V", &Hyper::lambda_V) - .def(py::pickle( - [](const Hyper &hyper) { - Real alpha = hyper.alpha; - Vector mu_w(hyper.mu_w); - Vector lambda_w(hyper.lambda_w); - DenseMatrix mu_V(hyper.mu_V); - DenseMatrix lambda_V(hyper.lambda_V); - return py::make_tuple(alpha, mu_w, lambda_w, mu_V, lambda_V); - }, - [](py::tuple t) { - if (t.size() != 5) { - throw std::runtime_error("invalid state for FMHyperParameters."); - } - // placement new - return new Hyper(t[0].cast(), t[1].cast(), - t[2].cast(), t[3].cast(), - t[4].cast()); - })); + ); + }); - py::class_(m, "VariationalFMHyperParameters") - .def_readonly("alpha", &VHyper::alpha) - .def_readonly("alpha_rate", &VHyper::alpha_rate) - .def_readonly("mu_w", &VHyper::mu_w) - .def_readonly("mu_w_var", &VHyper::mu_w_var) - .def_readonly("lambda_w", &VHyper::lambda_w) - .def_readonly("lambda_w_rate", &VHyper::lambda_w_rate) - .def_readonly("mu_V", &VHyper::mu_V) - .def_readonly("mu_V_var", &VHyper::mu_V_var) - .def_readonly("lambda_V", &VHyper::lambda_V) - .def_readonly("lambda_V_rate", &VHyper::lambda_V_rate) - .def(py::pickle( - [](const VHyper &hyper) { - Real alpha = hyper.alpha; - Real alpha_rate = hyper.alpha_rate; - Vector mu_w(hyper.mu_w); - Vector mu_w_var(hyper.mu_w_var); - Vector lambda_w(hyper.lambda_w); - Vector lambda_w_rate(hyper.lambda_w_rate); - DenseMatrix mu_V(hyper.mu_V); - DenseMatrix mu_V_var(hyper.mu_V_var); - DenseMatrix lambda_V(hyper.lambda_V); - DenseMatrix lambda_V_rate(hyper.lambda_V_rate); + nanobind::class_(m, "FMHyperParameters") + .def_ro("alpha", &Hyper::alpha) + .def_ro("mu_w", &Hyper::mu_w) + .def_ro("lambda_w", &Hyper::lambda_w) + .def_ro("mu_V", &Hyper::mu_V) + .def_ro("lambda_V", &Hyper::lambda_V) + .def("__getstate__", + [](const Hyper &hyper) { + Real alpha = hyper.alpha; + Vector mu_w(hyper.mu_w); + Vector lambda_w(hyper.lambda_w); + DenseMatrix mu_V(hyper.mu_V); + DenseMatrix lambda_V(hyper.lambda_V); + return std::make_tuple(alpha, mu_w, lambda_w, mu_V, lambda_V); + }) + .def("__setstate__", + [](Hyper &hyper, const std::tuple &state) { + new (&hyper) Hyper(std::get<0>(state), std::get<1>(state), + std::get<2>(state), std::get<3>(state), + std::get<4>(state)); + }); - return py::make_tuple(alpha, alpha_rate, mu_w, mu_w_var, lambda_w, - lambda_w_rate, mu_V, mu_V_var, lambda_V, - lambda_V_rate); - }, - [](py::tuple t) { - if (t.size() != 10) { - throw std::runtime_error("invalid state for FMHyperParameters."); - } - // placement new - return new VHyper( - t[0].cast(), t[1].cast(), t[2].cast(), - t[3].cast(), t[4].cast(), t[5].cast(), - t[6].cast(), t[7].cast(), - t[8].cast(), t[9].cast()); - })); + nanobind::class_(m, "VariationalFMHyperParameters") + .def_ro("alpha", &VHyper::alpha) + .def_ro("alpha_rate", &VHyper::alpha_rate) + .def_ro("mu_w", &VHyper::mu_w) + .def_ro("mu_w_var", &VHyper::mu_w_var) + .def_ro("lambda_w", &VHyper::lambda_w) + .def_ro("lambda_w_rate", &VHyper::lambda_w_rate) + .def_ro("mu_V", &VHyper::mu_V) + .def_ro("mu_V_var", &VHyper::mu_V_var) + .def_ro("lambda_V", &VHyper::lambda_V) + .def_ro("lambda_V_rate", &VHyper::lambda_V_rate) + .def("__getstate__", + [](const VHyper &hyper) { + Real alpha = hyper.alpha; + Real alpha_rate = hyper.alpha_rate; + Vector mu_w(hyper.mu_w); + Vector mu_w_var(hyper.mu_w_var); + Vector lambda_w(hyper.lambda_w); + Vector lambda_w_rate(hyper.lambda_w_rate); + DenseMatrix mu_V(hyper.mu_V); + DenseMatrix mu_V_var(hyper.mu_V_var); + DenseMatrix lambda_V(hyper.lambda_V); + DenseMatrix lambda_V_rate(hyper.lambda_V_rate); - py::class_(m, "Predictor") - .def_readonly("samples", &Predictor::samples) + return nanobind::make_tuple(alpha, alpha_rate, mu_w, mu_w_var, + lambda_w, lambda_w_rate, mu_V, + mu_V_var, lambda_V, lambda_V_rate); + }) + .def( + "__setstate__", + [](VHyper &hyper, const std::tuple &state) { + new (&hyper) VHyper(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), std::get<7>(state), + std::get<8>(state), std::get<9>(state)); + }); + + nanobind::class_(m, "Predictor") + .def_ro("samples", &Predictor::samples) .def("predict", &Predictor::predict) .def("predict_parallel", &Predictor::predict_parallel) .def("predict_parallel_oprobit", &Predictor::predict_parallel_oprobit) - .def(py::pickle( - [](const Predictor &predictor) { - return py::make_tuple(predictor.rank, predictor.feature_size, - static_cast(predictor.type), - predictor.samples); - }, - [](py::tuple t) { - if (t.size() != 4) { - throw std::runtime_error("invalid state for FMHyperParameters."); - } - Predictor *p = - new Predictor(t[0].cast(), t[1].cast(), - static_cast(t[2].cast())); - p->set_samples(std::move(t[3].cast>())); - return p; - })); - - py::class_(m, "VariationalPredictor") + .def("__getstate__", + [](const Predictor &predictor) { + return std::make_tuple(predictor.rank, predictor.feature_size, + static_cast(predictor.type), + predictor.samples); + }) + .def("__setstate__", + [](Predictor &predictor, + const std::tuple> &state) { + new (&predictor) Predictor(std::get<0>(state), std::get<1>(state), + std::get<2>(state), std::get<3>(state)); + }); + nanobind::class_(m, "VariationalPredictor") .def("predict", &VPredictor::predict) - .def(py::pickle( - [](const VPredictor &predictor) { - return py::make_tuple(predictor.rank, predictor.feature_size, - static_cast(predictor.type), - predictor.samples); - }, - [](py::tuple t) { - if (t.size() != 4) { - throw std::runtime_error("invalid state for FMHyperParameters."); - } - VPredictor *p = - new VPredictor(t[0].cast(), t[1].cast(), - static_cast(t[2].cast())); - p->set_samples(std::move(t[3].cast>())); - return p; - })) - .def("weights", [](VPredictor &predictor) { - VFM returned = predictor.samples.at(0); - return returned; - }); - - py::class_(m, "FMTrainer") - .def(py::init &, - const Vector &, int, FMLearningConfig>()) + .def("__getstate__", + [](const VPredictor &predictor) { + return std::make_tuple(predictor.rank, predictor.feature_size, + static_cast(predictor.type), + predictor.samples); + }) + .def("__setstate__", + [](VPredictor &predictor, + const std::tuple> &state) { + new (&predictor) VPredictor(std::get<0>(state), std::get<1>(state), + std::get<2>(state), std::get<3>(state)); + }); + nanobind::class_(m, "FMTrainer") + .def(nanobind::init &, + const Vector &, int, FMLearningConfig>()) .def("create_FM", &FMTrainer::create_FM) .def("create_Hyper", &FMTrainer::create_Hyper); - py::class_(m, "VariationalFMTrainer") - .def(py::init &, - const Vector &, int, FMLearningConfig>()) + nanobind::class_(m, "VariationalFMTrainer") + .def(nanobind::init &, + const Vector &, int, FMLearningConfig>()) .def("create_FM", &VFMTrainer::create_FM) .def("create_Hyper", &VFMTrainer::create_Hyper); - py::class_(m, "LearningHistory") - .def_readonly("hypers", &History::hypers) - .def_readonly("train_log_losses", &History::train_log_losses) - .def_readonly("n_mh_accept", &History::n_mh_accept) - .def(py::pickle( - [](const History &h) { - return py::make_tuple(h.hypers, h.train_log_losses, h.n_mh_accept); - }, - [](py::tuple t) { - if (t.size() != 3) { - throw std::runtime_error("invalid state for LearningHistory."); - } - History *result = new History(); - result->hypers = t[0].cast>(); - result->train_log_losses = t[1].cast>(); - result->n_mh_accept = t[2].cast>(); - return result; - })); + nanobind::class_(m, "LearningHistory") + .def_ro("hypers", &History::hypers) + .def_ro("train_log_losses", &History::train_log_losses) + .def_ro("n_mh_accept", &History::n_mh_accept) + .def("__getstate__", + [](const History &h) { + return std::make_tuple(h.hypers, h.train_log_losses, + h.n_mh_accept); + }) + .def("__setstate__", + [](History &h, const std::tuple, vector, + vector> &state) { + History *result = new History(); + new (&h) History(); + h.hypers = std::get<0>(state); + h.train_log_losses = std::get<1>(state); + h.n_mh_accept = std::get<2>(state); + }); + + nanobind::class_(m, "VariationalLearningHistory") + .def_ro("hypers", &VHistory::hyper) + .def_ro("elbos", &VHistory::elbos) + .def("__getstate__", + [](const VHistory &h) { return std::make_tuple(h.hyper, h.elbos); }) + .def("__setstate__", + + [](VHistory &result, const std::tuple> &state) { + new (&result) VHistory(std::get<0>(state), std::get<1>(state) - py::class_(m, "VariationalLearningHistory") - .def_readonly("hypers", &VHistory::hyper) - .def_readonly("elbos", &VHistory::elbos) - .def(py::pickle( - [](const VHistory &h) { return py::make_tuple(h.hyper, h.elbos); }, - [](py::tuple t) { - if (t.size() != 2) { - throw std::runtime_error( - "invalid state for VariationalLearningHistory."); - } - VHistory *result = - new VHistory(t[0].cast(), t[1].cast>()); - return result; - })); + ); + }); m.def("create_train_fm", &create_train_fm, "create and train fm.", - py::return_value_policy::move); + nanobind::rv_policy::move); m.def("create_train_vfm", &create_train_vfm, "create and train fm.", - py::return_value_policy::move, py::arg("rank"), py::arg("init_std"), - py::arg("X"), py::arg("relations"), py::arg("y"), - py::arg("random_seed"), py::arg("learning_config"), - py::arg("callback")); + nanobind::rv_policy::move, nanobind::arg("rank"), + nanobind::arg("init_std"), nanobind::arg("X"), + nanobind::arg("relations"), nanobind::arg("y"), + nanobind::arg("random_seed"), nanobind::arg("learning_config"), + nanobind::arg("callback")); m.def("mean_var_truncated_normal_left", &myFM::mean_var_truncated_normal_left); m.def("mean_var_truncated_normal_right", diff --git a/include/myfm/predictor.hpp b/include/myfm/predictor.hpp index d3ddd8e..f891315 100644 --- a/include/myfm/predictor.hpp +++ b/include/myfm/predictor.hpp @@ -6,7 +6,6 @@ #include "FM.hpp" #include "FMLearningConfig.hpp" -#include "definitions.hpp" #include "util.hpp" namespace myFM { @@ -21,6 +20,9 @@ template > struct Predictor { inline Predictor(size_t rank, size_t feature_size, TASKTYPE type) : rank(rank), feature_size(feature_size), type(type), samples() {} + inline Predictor(size_t rank, size_t feature_size, TASKTYPE type, const vector & samples) + : rank(rank), feature_size(feature_size), type(type), samples(samples) {} + inline void check_input(const SparseMatrix &X, const vector &relations) const { auto given_feature_size = check_row_consistency_return_column(X, relations); @@ -146,8 +148,9 @@ template > struct Predictor { return result; } - inline void set_samples(vector &&samples_from) { - samples = std::forward>(samples_from); + inline void set_samples(const vector &samples_from) { + samples.clear(); + std::copy(samples_from.begin(),samples_from.end(), samples.begin()); } inline void add_sample(const FMType &fm) { diff --git a/include/myfm/variational.hpp b/include/myfm/variational.hpp index 789c5a7..efeb6bb 100644 --- a/include/myfm/variational.hpp +++ b/include/myfm/variational.hpp @@ -1,12 +1,7 @@ #pragma once -#include #include -#include -#include -#include #include -#include #include #include "FM.hpp" diff --git a/pyproject.toml b/pyproject.toml index 98476cb..735073f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,21 +1,43 @@ -[build-system] -requires = [ - "setuptools>=42", - "wheel", - "pybind11>=2.8.0", - "httpx", - "setuptools_scm[toml]>=6.2", +[project] +name = "myfm" +license = "MIT" +dynamic = ["version"] +description = "Yet another Bayesian factorization machines." +readme = "README.md" +authors = [ + { name = "Tomoki Ohtsuki", email = "tomoki.ohtsuki.19937@outlook.jp" }, +] +dependencies = [ + "numpy>=2.0", + "scipy>=1.0", + "tqdm>=4", + "pandas>=2.2.0", + "typing-extensions>=4.0.0", ] -build-backend = "setuptools.build_meta" +[project.urls] +Homepage = "https://github.com/tohtsky/myFM" + + + +[build-system] +requires = ["scikit-build-core >=0.4.3", "nanobind >=1.3.2", "requests", "setuptools_scm", "setuptools>=64", "wheel"] +build-backend = "scikit_build_core.build" + +[tool.scikit-build] +minimum-version = "0.4" +build-dir = "build/{wheel_tag}" +wheel.py-api = "cp312" +metadata.version.provider = "scikit_build_core.metadata.setuptools_scm" +sdist.include = ["src/myfm/_version.py"] + + +[tool.setuptools_scm] +write_to = "src/myfm/_version.py" +local_scheme = "no-local-version" + [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 @@ -28,3 +50,13 @@ use_parentheses = true [tool.pycln] all = true + + +[tool.cibuildwheel] +build-verbosity = 1 +skip = ["cp38-*", "pp38-*"] +archs = ["auto64"] + +[tool.cibuildwheel.linux] +# Export a variable +environment-pass = ["CFLAGS", "CXXFLAGS"] diff --git a/setup.py b/setup.py deleted file mode 100644 index 4c26abd..0000000 --- a/setup.py +++ /dev/null @@ -1,101 +0,0 @@ -import os -from pathlib import Path -from typing import Any - -from pybind11.setup_helpers import Pybind11Extension, build_ext -from setuptools import find_packages, setup - -install_requires = [ - "numpy>=1.11", - "scipy>=1.0", - "tqdm>=4", - "pandas>=1.0.0", - "typing-extensions>=4.0.0", -] - -CURRENT_DIR = Path(__file__).resolve().parent -README_FILE = CURRENT_DIR / "README.md" - - -class get_eigen_include(object): - EIGEN3_URL = "https://gitlab.com/libeigen/eigen/-/archive/3.4.0/eigen-3.4.0.zip" - EIGEN3_DIRNAME = "eigen-3.4.0" - - 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 - - basedir = Path(__file__).resolve().parent - target_dir = basedir / self.EIGEN3_DIRNAME - if target_dir.exists(): - return str(target_dir) - - download_target_dir = basedir / "eigen3.zip" - import zipfile - - import httpx - - print("Start downloading Eigen library from {}.".format(self.EIGEN3_DIRNAME)) - with httpx.stream("GET", self.EIGEN3_URL, verify=False) as response: - with download_target_dir.open("wb") as ofs: - for chunk in response.iter_bytes(chunk_size=1024): - ofs.write(chunk) - print("Downloaded Eigen into {}.".format(download_target_dir)) - - with zipfile.ZipFile(download_target_dir) as ifs: - ifs.extractall() - - return str(target_dir) - - -headers = [ - "include/myfm/definitions.hpp", - "include/myfm/util.hpp", - "include/myfm/FM.hpp", - "include/myfm/HyperParams.hpp", - "include/myfm/predictor.hpp", - "include/myfm/FMTrainer.hpp", - "include/myfm/FMLearningConfig.hpp", - "include/myfm/OProbitSampler.hpp", - "include/Faddeeva/Faddeeva.hh", - "cpp_source/declare_module.hpp", -] - - -ext_modules = [ - Pybind11Extension( - "myfm._myfm", - ["cpp_source/bind.cpp", "cpp_source/Faddeeva.cc"], - include_dirs=[ - # Path to pybind11 headers - get_eigen_include(), - "include", - ], - ), -] - - -def local_scheme(version: Any) -> str: - return "" - - -setup( - name="myfm", - use_scm_version={"local_scheme": local_scheme}, - author="Tomoki Ohtsuki", - url="https://github.com/tohtsky/myfm", - author_email="tomoki.ohtsuki.19937@outlook.jp", - description="Yet another Bayesian factorization machines.", - long_description=README_FILE.read_text(), - long_description_content_type="text/markdown", - ext_modules=ext_modules, - install_requires=install_requires, - cmdclass={"build_ext": build_ext}, - package_dir={"": "src"}, - zip_safe=False, - headers=headers, - python_requires=">=3.6", - packages=find_packages("src"), - package_data={"myfm": ["*.pyi"]}, -) diff --git a/src/myfm/gibbs.py b/src/myfm/gibbs.py index 1dee991..ca0e37a 100644 --- a/src/myfm/gibbs.py +++ b/src/myfm/gibbs.py @@ -46,7 +46,7 @@ def w0_samples(self) -> Optional[DenseArray]: """ if self.predictor_ is None: return None - return np.asfarray([fm.w0 for fm in self.predictor_.samples]) + return np.asarray([fm.w0 for fm in self.predictor_.samples]) @property def w_samples(self) -> Optional[DenseArray]: @@ -58,7 +58,7 @@ def w_samples(self) -> Optional[DenseArray]: """ if self.predictor_ is None: return None - return np.asfarray([fm.w for fm in self.predictor_.samples]) + return np.asarray([fm.w for fm in self.predictor_.samples]) @property def V_samples(self) -> Optional[DenseArray]: @@ -70,7 +70,7 @@ def V_samples(self) -> Optional[DenseArray]: """ if self.predictor_ is None: return None - return np.asfarray([fm.V for fm in self.predictor_.samples]) + return np.asarray([fm.V for fm in self.predictor_.samples]) def _predict_core( self, @@ -540,4 +540,4 @@ def cutpoint_samples(self) -> Optional[DenseArray]: """ if self.predictor_ is None: return None - return np.asfarray([fm.cutpoints[0] for fm in self.predictor_.samples]) + return np.asarray([fm.cutpoints[0] for fm in self.predictor_.samples]) diff --git a/src/myfm/utils/encoders/binning.py b/src/myfm/utils/encoders/binning.py index 52445ae..5cadebe 100644 --- a/src/myfm/utils/encoders/binning.py +++ b/src/myfm/utils/encoders/binning.py @@ -32,7 +32,7 @@ def __init__(self, x: ArrayLike, n_percentiles: int = 10) -> None: if n_percentiles <= 0: raise ValueError("n_percentiles must be greater than 0.") self.percentages = np.linspace(0, 100, n_percentiles + 2)[1:-1] - x_arr = np.asfarray(x) + x_arr = np.asarray(x) temp_percentiles: DenseArray = np.percentile( x_arr[~np.isnan(x_arr)], self.percentages ) diff --git a/tests/conftest.py b/tests/conftest.py index 3e003e8..81080c8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,8 +14,8 @@ def stub_weight() -> FMWeights: weights = FMWeights( -3.0, - np.asfarray([1.0, 2.0, -1.0]), - np.asfarray( + np.asarray([1.0, 2.0, -1.0]), + np.asarray( [[1.0, -1.0, 0], [0.0, 1.0, 1.0], [1.0, 1.0, 1.0], [-1.0, 0, -1.0]] ), ) diff --git a/tests/oprobit/test_oprobit_1dim.py b/tests/oprobit/test_oprobit_1dim.py index 6d655f5..0618538 100644 --- a/tests/oprobit/test_oprobit_1dim.py +++ b/tests/oprobit/test_oprobit_1dim.py @@ -9,7 +9,7 @@ @pytest.mark.parametrize("use_libfm_callback", [True, False]) def test_oprobit(use_libfm_callback: bool) -> None: N_train = 1000 - cps = np.asfarray([0.0, 0.5, 1.5]) + cps = np.asarray([0.0, 0.5, 1.5]) rns = np.random.RandomState(0) X = rns.normal(0, 2, size=N_train) coeff = 0.5 diff --git a/tests/utils/test_categorical.py b/tests/utils/test_categorical.py index 244bcfa..3504a12 100644 --- a/tests/utils/test_categorical.py +++ b/tests/utils/test_categorical.py @@ -31,7 +31,7 @@ def test_categorical_encs_create() -> None: assert len(enc_cutoff) == 3 X_cutoffed = enc_cutoff.to_sparse(["item4", "item1", "item2", "item3"]) np.testing.assert_allclose( - X_cutoffed.toarray(), np.asfarray([[1, 0, 0], [0, 1, 0], [0, 0, 1], [1, 0, 0]]) + X_cutoffed.toarray(), np.asarray([[1, 0, 0], [0, 1, 0], [0, 0, 1], [1, 0, 0]]) ) @@ -39,14 +39,14 @@ def test_categorical_encs_ignore() -> None: enc = CategoryValueToSparseEncoder(TEST_ITEMS, handle_unknown="ignore") X = enc.to_sparse(["item4", "item1", "item2", "item3"]) np.testing.assert_allclose( - X.toarray(), np.asfarray([[0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1]]) + X.toarray(), np.asarray([[0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1]]) ) enc_cutoff = CategoryValueToSparseEncoder( TEST_ITEMS, handle_unknown="ignore", min_freq=3 ) X = enc_cutoff.to_sparse(["item4", "item1", "item2", "item3"]) np.testing.assert_allclose( - X.toarray(), np.asfarray([[0, 0], [1, 0], [0, 1], [0, 0]]) + X.toarray(), np.asarray([[0, 0], [1, 0], [0, 1], [0, 0]]) ) @@ -57,5 +57,5 @@ def test_categorical_encs_raise() -> None: X = enc.to_sparse(["item1", "item2", "item3"]) np.testing.assert_allclose( - X.toarray(), np.asfarray([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) + X.toarray(), np.asarray([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) ) From 756a381cc22131005294612cc470cd04fde66130 Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Fri, 11 Jul 2025 14:43:25 +0900 Subject: [PATCH 02/23] .weights --- cpp_source/declare_module.hpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cpp_source/declare_module.hpp b/cpp_source/declare_module.hpp index 926d6f8..a1a888a 100644 --- a/cpp_source/declare_module.hpp +++ b/cpp_source/declare_module.hpp @@ -303,6 +303,10 @@ template void declare_functional(nanobind::module_ &m) { }); nanobind::class_(m, "VariationalPredictor") .def("predict", &VPredictor::predict) + .def("weights", [](const VPredictor &predictor) { + VFM returned = predictor.samples.at(0); + return returned; + }) .def("__getstate__", [](const VPredictor &predictor) { return std::make_tuple(predictor.rank, predictor.feature_size, From f31dff1e7eb4927862353be654fb629c07b88430 Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Fri, 11 Jul 2025 15:14:09 +0900 Subject: [PATCH 03/23] .weights --- include/myfm/predictor.hpp | 5 ----- src/myfm/__init__.py | 7 +++---- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/include/myfm/predictor.hpp b/include/myfm/predictor.hpp index f891315..fb24fe4 100644 --- a/include/myfm/predictor.hpp +++ b/include/myfm/predictor.hpp @@ -148,11 +148,6 @@ template > struct Predictor { return result; } - inline void set_samples(const vector &samples_from) { - samples.clear(); - std::copy(samples_from.begin(),samples_from.end(), samples.begin()); - } - inline void add_sample(const FMType &fm) { if (fm.w0.rows() != feature_size) { throw std::invalid_argument("feature size mismatch!"); diff --git a/src/myfm/__init__.py b/src/myfm/__init__.py index 05a7b78..d5b9a5e 100644 --- a/src/myfm/__init__.py +++ b/src/myfm/__init__.py @@ -1,10 +1,9 @@ from pkg_resources import DistributionNotFound, get_distribution # type: ignore try: - __version__ = get_distribution("myfm").version -except DistributionNotFound: # pragma: no cover - # package is not installed - pass # pragma: no cover + from ._version import __version__ +except: # pragma: no cover + __version__ = "0.0.0" from ._myfm import RelationBlock from .gibbs import MyFMGibbsClassifier, MyFMGibbsRegressor, MyFMOrderedProbit From da752e970168187ec11d16973d10a11ffa1289dc Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Fri, 11 Jul 2025 16:29:15 +0900 Subject: [PATCH 04/23] fix --- include/myfm/FMLearningConfig.hpp | 46 +++++++++++-------------------- src/myfm/base.py | 3 +- 2 files changed, 18 insertions(+), 31 deletions(-) diff --git a/include/myfm/FMLearningConfig.hpp b/include/myfm/FMLearningConfig.hpp index f26bd26..7ecf572 100644 --- a/include/myfm/FMLearningConfig.hpp +++ b/include/myfm/FMLearningConfig.hpp @@ -107,83 +107,69 @@ template struct FMLearningConfig { Builder() {} - inline Builder &set_alpha_0(Real arg) { + inline void set_alpha_0(Real arg) { this->alpha_0 = arg; - return *this; } - inline Builder &set_beta_0(Real arg) { + inline void set_beta_0(Real arg) { this->beta_0 = arg; - return *this; } - inline Builder &set_gamma_0(Real arg) { + inline void set_gamma_0(Real arg) { this->gamma_0 = arg; - return *this; } - inline Builder &set_mu_0(Real arg) { + inline void set_mu_0(Real arg) { this->mu_0 = arg; - return *this; } - inline Builder &set_reg_0(Real arg) { + inline void set_reg_0(Real arg) { this->reg_0 = arg; - return *this; } - inline Builder &set_n_iter(int arg) { + inline void set_n_iter(int arg) { this->n_iter = arg; - return *this; } - inline Builder &set_n_kept_samples(int arg) { + inline void set_n_kept_samples(int arg) { this->n_kept_samples = arg; - return *this; } - inline Builder &set_task_type(TASKTYPE arg) { + inline void set_task_type(TASKTYPE arg) { this->task_type = arg; - return *this; } - inline Builder &set_group_index(const vector arg) { + inline void set_group_index(const vector arg) { this->group_index = arg; - return *this; } - inline Builder &set_identical_groups(size_t n_features) { + inline void set_identical_groups(size_t n_features) { vector default_group_index(n_features); for (auto c = default_group_index.begin(); c != default_group_index.end(); c++) { *c = 0; } - return set_group_index(default_group_index); + set_group_index(default_group_index); } - inline Builder &set_nu_oprobit(size_t nu_oprobit) { + inline void set_nu_oprobit(size_t nu_oprobit) { this->nu_oprobit = nu_oprobit; - return *this; } - inline Builder &set_fit_w0(bool fit_w0) { + inline void set_fit_w0(bool fit_w0) { this->fit_w0 = fit_w0; - return *this; } - inline Builder &set_fit_linear(bool fit_linear) { + inline void set_fit_linear(bool fit_linear) { this->fit_linear = fit_linear; - return *this; } - inline Builder &set_cutpoint_scale(Real cutpoint_scale) { + inline void set_cutpoint_scale(Real cutpoint_scale) { this->cutpoint_scale = cutpoint_scale; - return *this; } - inline Builder & + inline void set_cutpoint_groups(const CutpointGroupType &cutpoint_groups) { this->cutpoint_groups = cutpoint_groups; - return *this; } FMLearningConfig build() { diff --git a/src/myfm/base.py b/src/myfm/base.py index 2b78f86..02abc84 100644 --- a/src/myfm/base.py +++ b/src/myfm/base.py @@ -280,7 +280,8 @@ def _fit( else: do_test = False - config_builder.set_n_iter(n_iter).set_n_kept_samples(n_kept_samples) + config_builder.set_n_iter(n_iter) + config_builder.set_n_kept_samples(n_kept_samples) if X.dtype != np.float64: X = X.astype(np.float64) From 3b1b60c336bdb9ee0b296afa0ee4ecf1704610b8 Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Fri, 11 Jul 2025 18:33:42 +0900 Subject: [PATCH 05/23] pickling test --- .gitignore | 4 +++- tests/classification/test_classification.py | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 6102ae7..2329382 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,6 @@ stubs/* doc/source/api_reference/*.rst .cache -src/myfm/_version.py \ No newline at end of file +src/myfm/_version.py + +mise.toml \ No newline at end of file diff --git a/tests/classification/test_classification.py b/tests/classification/test_classification.py index 3240227..e82203c 100644 --- a/tests/classification/test_classification.py +++ b/tests/classification/test_classification.py @@ -1,5 +1,8 @@ from typing import Tuple +import pickle +from io import BytesIO + import numpy as np import pytest from scipy import sparse as sps @@ -32,6 +35,12 @@ def test_middle_clf( if use_libfm_callback: np.testing.assert_allclose(fm.predict_proba(X), callback.predictions / 200) + stream = BytesIO() + pickle.dump(fm, stream) + stream.seek(0) + read_fm = pickle.load(stream) + np.testing.assert_allclose(fm.predict_proba(X), read_fm.predict_proba(X)) + vfm_before_fit = VariationalFMClassifier(3) assert vfm_before_fit.w0_mean is None assert vfm_before_fit.w0_var is None @@ -53,6 +62,14 @@ def test_middle_clf( assert fm.predictor_ is not None + vfm_stream = BytesIO() + pickle.dump(vfm, vfm_stream) + vfm_stream.seek(0) + read_vfm = pickle.load(vfm_stream) + np.testing.assert_allclose(vfm.predict_proba(X), read_vfm.predict_proba(X)) + + vfm.predict_proba(X) + last_samples = fm.predictor_.samples[-20:] for i in range(3): From a950063eac544affc9620924c0b70fb73a3a4c48 Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Fri, 11 Jul 2025 18:38:28 +0900 Subject: [PATCH 06/23] wheels.yml --- .github/workflows/wheels.yml | 163 ++++++++--------------------------- 1 file changed, 37 insertions(+), 126 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 56491e0..e9da673 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -1,166 +1,77 @@ name: Build wheel on: push: - branches: - - main +# branches: +# - main release: types: - created env: - cibuildwheel_version: "2.12.2" + cibuildwheel_version: '3.0.1' jobs: build_sdist: name: Build source distribution runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 + - uses: actions/checkout@v4 - uses: actions/setup-python@v3 name: Install Python with: - python-version: "3.11" + python-version: '3.13' - name: Build sdist - run: pip install pybind11 && python setup.py sdist - - uses: actions/upload-artifact@v2 + run: pip install build && python -m build --sdist + - uses: actions/upload-artifact@v4 with: path: dist/*.tar.gz build_wheels: name: Build wheels on ${{ matrix.os }} - runs-on: ${{ matrix.os }} - env: - MACOSX_DEPLOYMENT_TARGET: "10.9" - CIBW_BUILD_VERBOSITY: "1" - CIBW_BUILD: "${{ matrix.cibw.build || '*' }}" - CIBW_SKIP: "${{ matrix.cibw.skip || '' }}" - CIBW_ENVIRONMENT: "${{ matrix.cibw.env || '' }}" - CIBW_TEST_COMMAND: "pytest {project}/tests" - CIBW_TEST_REQUIRES: pytest pytest-mock - CIBW_MANYLINUX_X86_64_IMAGE: "${{ matrix.cibw.manylinux_image }}" - CIBW_MANYLINUX_I686_IMAGE: "${{ matrix.cibw.manylinux_image }}" - CIBW_MANYLINUX_AARCH64_IMAGE: "${{ matrix.cibw.manylinux_image }}" - CIBW_ARCHS_LINUX: "${{ matrix.cibw.arch || 'auto' }}" - CIBW_ARCHS_MACOS: "${{ matrix.cibw.arch || 'auto' }}" + runs-on: ${{ matrix.runs-on }} strategy: matrix: include: - - os: macos-10.15 - name: mac - cibw: - arch: x86_64 - build: "cp37* cp38*" - - - os: macos-10.15 - name: mac-arm - cibw: - arch: universal2 - build: "cp39* cp310* cp311*" - - - os: ubuntu-20.04 - name: manylinux1 - cibw: - build: "cp37*" - skip: "*musllinux*" - manylinux_image: manylinux2010 - arch: auto64 - - - os: ubuntu-20.04 - name: manylinux2014 - cibw: - build: "cp38* cp39* cp310* cp311*" - skip: "*musllinux*" - manylinux_image: manylinux2014 - arch: auto64 - - - os: ubuntu-20.04 - name: manylinux_aarch64_cp37 - cibw: - build: "cp37*" - skip: "*musllinux*" - manylinux_image: manylinux2014 - arch: aarch64 - - - os: ubuntu-20.04 - name: manylinux_aarch64_cp38 - cibw: - build: "cp38*" - skip: "*musllinux*" - manylinux_image: manylinux2014 - arch: aarch64 - - - os: ubuntu-20.04 - name: manylinux_aarch64_cp39 - cibw: - build: "cp39*" - skip: "*musllinux*" - manylinux_image: manylinux2014 - arch: aarch64 - - - os: ubuntu-20.04 - name: manylinux_aarch64_cp310 - cibw: - build: "cp310*" - skip: "*musllinux*" - manylinux_image: manylinux2014 - arch: aarch64 - - - os: ubuntu-20.04 - name: manylinux_aarch64_cp311 - cibw: - build: "cp311*" - skip: "*musllinux*" - manylinux_image: manylinux2014 - arch: aarch64 - - - os: windows-2019 - name: win_amd64 - architecture: x64 - cibw: - skip: "cp36*" - build: "cp*win_amd64" - + - os: linux-intel + runs-on: ubuntu-latest + - os: linux-arm + runs-on: ubuntu-24.04-arm + - os: windows-intel + runs-on: windows-latest + - os: macos-intel + runs-on: macos-13 + - os: macos-arm + runs-on: macos-latest steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - uses: actions/setup-python@v2 - name: Install Python - - name: register qemu - if: contains(matrix.cibw.arch, 'aarch64') - run: | - docker run --rm --privileged hypriot/qemu-register:v4.2.0 - - name: Install cibuildwheel - run: python -m pip install cibuildwheel=="${{env.cibuildwheel_version}}" + - uses: actions/checkout@v4 - name: Build wheels - run: python -m cibuildwheel --output-dir wheelhouse - - - uses: actions/upload-artifact@v2 + uses: pypa/cibuildwheel@v3.0.1 + env: + CIBW_PLATFORM: ${{ matrix.platform || 'auto' }} + - uses: actions/upload-artifact@v4 with: + name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }} path: ./wheelhouse/*.whl - upload_pypi: needs: [build_wheels, build_sdist] runs-on: ubuntu-latest + permissions: + id-token: write steps: - - uses: actions/download-artifact@v2 + - uses: actions/download-artifact@v4 with: - name: artifact + 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/ + packages-dir: dist/ + repository-url: https://test.pypi.org/legacy/ verbose: true - skip_existing: true + skip-existing: true + attestations: false - 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/ + packages-dir: dist/ verbose: true - skip_existing: true + skip-existing: true \ No newline at end of file From 2fc6efcad3ddc46b86f0779e6af7e97204696d86 Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Fri, 11 Jul 2025 18:42:26 +0900 Subject: [PATCH 07/23] compile error --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 735073f..d14a3f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,3 +60,7 @@ archs = ["auto64"] [tool.cibuildwheel.linux] # Export a variable environment-pass = ["CFLAGS", "CXXFLAGS"] + +# Needed for full C++17 support +[tool.cibuildwheel.macos.environment] +MACOSX_DEPLOYMENT_TARGET = "10.14" \ No newline at end of file From f0bba1f1d25e6c7add3b8865db4c168706512988 Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Fri, 11 Jul 2025 18:46:41 +0900 Subject: [PATCH 08/23] pre-commit --- .github/workflows/pre-commit.yml | 6 +-- .github/workflows/wheels.yml | 2 +- .gitignore | 2 +- .pre-commit-config.yaml | 18 ++++----- CMakeLists.txt | 2 +- include/myfm/FMLearningConfig.hpp | 2 +- mypy.ini | 4 +- pyproject.toml | 2 +- src/myfm/_myfm.pyi | 42 +++++++++++++++++++++ tests/classification/test_classification.py | 7 ++-- tests/conftest.py | 4 +- 11 files changed, 63 insertions(+), 28 deletions(-) diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 9cacefa..714f374 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/setup-python@v3 - - uses: pre-commit/action@v3.0.0 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v3 + - uses: pre-commit/action@v3.0.0 \ No newline at end of file diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index e9da673..c51925b 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -74,4 +74,4 @@ jobs: with: packages-dir: dist/ verbose: true - skip-existing: true \ No newline at end of file + skip-existing: true diff --git a/.gitignore b/.gitignore index 2329382..d0f8494 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,4 @@ doc/source/api_reference/*.rst .cache src/myfm/_version.py -mise.toml \ No newline at end of file +mise.toml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 37e88d0..d7a3e55 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.2.0 + rev: v4.6.0 hooks: - id: check-merge-conflict - id: check-yaml @@ -10,20 +10,18 @@ repos: - id: no-commit-to-branch args: [--branch, main] - id: trailing-whitespace - - id: end-of-file-fixer - id: check-added-large-files - repo: https://github.com/PyCQA/isort - rev: 5.12.0 + rev: 5.13.2 hooks: - id: isort name: isort + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.13.0 + hooks: + - id: mypy + additional_dependencies: ['types-requests', 'types-setuptools'] - repo: https://github.com/psf/black - rev: 22.3.0 + rev: 24.10.0 hooks: - id: black - language_version: python3 # Should be a command that runs python3.6+ - - repo: https://github.com/hadialqattan/pycln - rev: v1.1.0 - hooks: - - id: pycln - args: [--config=pyproject.toml] diff --git a/CMakeLists.txt b/CMakeLists.txt index 03120a3..d42fd77 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,4 +39,4 @@ find_package(nanobind CONFIG REQUIRED) nanobind_add_module(_myfm cpp_source/bind.cpp cpp_source/Faddeeva.cc) -install(TARGETS _myfm LIBRARY DESTINATION myfm/) \ No newline at end of file +install(TARGETS _myfm LIBRARY DESTINATION myfm/) diff --git a/include/myfm/FMLearningConfig.hpp b/include/myfm/FMLearningConfig.hpp index 7ecf572..3f0e8af 100644 --- a/include/myfm/FMLearningConfig.hpp +++ b/include/myfm/FMLearningConfig.hpp @@ -167,7 +167,7 @@ template struct FMLearningConfig { this->cutpoint_scale = cutpoint_scale; } - inline void + inline void set_cutpoint_groups(const CutpointGroupType &cutpoint_groups) { this->cutpoint_groups = cutpoint_groups; } diff --git a/mypy.ini b/mypy.ini index 8d5c290..e66be81 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2,7 +2,7 @@ # Specify the target platform details in config, so your developers are # free to run mypy on Windows, Linux, or macOS and get consistent # results. -python_version=3.6 +python_version=3.13 platform=linux show_column_numbers=True @@ -27,5 +27,3 @@ check_untyped_defs=True # No incremental mode cache_dir=/dev/null - -plugins = numpy.typing.mypy_plugin diff --git a/pyproject.toml b/pyproject.toml index d14a3f4..d64008f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,4 +63,4 @@ environment-pass = ["CFLAGS", "CXXFLAGS"] # Needed for full C++17 support [tool.cibuildwheel.macos.environment] -MACOSX_DEPLOYMENT_TARGET = "10.14" \ No newline at end of file +MACOSX_DEPLOYMENT_TARGET = "10.14" diff --git a/src/myfm/_myfm.pyi b/src/myfm/_myfm.pyi index 51de1d4..101f595 100644 --- a/src/myfm/_myfm.pyi +++ b/src/myfm/_myfm.pyi @@ -1,4 +1,5 @@ """Backend C++ implementation for myfm.""" + from typing import Iterable as iterable from typing import Iterator as iterator from typing import * @@ -68,30 +69,37 @@ class FM: """ :type: npt.NDArray[float64] """ + @V.setter def V(self, arg0: npt.NDArray[float64]) -> None: pass + @property def cutpoints(self) -> List[npt.NDArray[float64]]: """ :type: List[npt.NDArray[float64]] """ + @cutpoints.setter def cutpoints(self, arg0: List[npt.NDArray[float64]]) -> None: pass + @property def w(self) -> npt.NDArray[float64]: """ :type: npt.NDArray[float64] """ + @w.setter def w(self, arg0: npt.NDArray[float64]) -> None: pass + @property def w0(self) -> float: """ :type: float """ + @w0.setter def w0(self, arg0: float) -> None: pass @@ -105,21 +113,25 @@ class FMHyperParameters: """ :type: float """ + @property def lambda_V(self) -> npt.NDArray[float64]: """ :type: npt.NDArray[float64] """ + @property def lambda_w(self) -> npt.NDArray[float64]: """ :type: npt.NDArray[float64] """ + @property def mu_V(self) -> npt.NDArray[float64]: """ :type: npt.NDArray[float64] """ + @property def mu_w(self) -> npt.NDArray[float64]: """ @@ -151,11 +163,13 @@ class LearningHistory: """ :type: List[FMHyperParameters] """ + @property def n_mh_accept(self) -> List[int]: """ :type: List[int] """ + @property def train_log_losses(self) -> List[float]: """ @@ -215,6 +229,7 @@ class RelationBlock: ----- The entries of `original_to_block` must be in the [0, data.shape[0]-1]. """ + def __repr__(self) -> str: ... def __setstate__(self, arg0: tuple) -> None: ... @property @@ -222,21 +237,25 @@ class RelationBlock: """ :type: int """ + @property def data(self) -> scipy.sparse.csr_matrix[float64]: """ :type: scipy.sparse.csr_matrix[float64] """ + @property def feature_size(self) -> int: """ :type: int """ + @property def mapper_size(self) -> int: """ :type: int """ + @property def original_to_block(self) -> List[int]: """ @@ -283,54 +302,67 @@ class VariationalFM: """ :type: npt.NDArray[float64] """ + @V.setter def V(self, arg0: npt.NDArray[float64]) -> None: pass + @property def V_var(self) -> npt.NDArray[float64]: """ :type: npt.NDArray[float64] """ + @V_var.setter def V_var(self, arg0: npt.NDArray[float64]) -> None: pass + @property def cutpoints(self) -> List[npt.NDArray[float64]]: """ :type: List[npt.NDArray[float64]] """ + @cutpoints.setter def cutpoints(self, arg0: List[npt.NDArray[float64]]) -> None: pass + @property def w(self) -> npt.NDArray[float64]: """ :type: npt.NDArray[float64] """ + @w.setter def w(self, arg0: npt.NDArray[float64]) -> None: pass + @property def w0(self) -> float: """ :type: float """ + @w0.setter def w0(self, arg0: float) -> None: pass + @property def w0_var(self) -> float: """ :type: float """ + @w0_var.setter def w0_var(self, arg0: float) -> None: pass + @property def w_var(self) -> npt.NDArray[float64]: """ :type: npt.NDArray[float64] """ + @w_var.setter def w_var(self, arg0: npt.NDArray[float64]) -> None: pass @@ -344,46 +376,55 @@ class VariationalFMHyperParameters: """ :type: float """ + @property def alpha_rate(self) -> float: """ :type: float """ + @property def lambda_V(self) -> npt.NDArray[float64]: """ :type: npt.NDArray[float64] """ + @property def lambda_V_rate(self) -> npt.NDArray[float64]: """ :type: npt.NDArray[float64] """ + @property def lambda_w(self) -> npt.NDArray[float64]: """ :type: npt.NDArray[float64] """ + @property def lambda_w_rate(self) -> npt.NDArray[float64]: """ :type: npt.NDArray[float64] """ + @property def mu_V(self) -> npt.NDArray[float64]: """ :type: npt.NDArray[float64] """ + @property def mu_V_var(self) -> npt.NDArray[float64]: """ :type: npt.NDArray[float64] """ + @property def mu_w(self) -> npt.NDArray[float64]: """ :type: npt.NDArray[float64] """ + @property def mu_w_var(self) -> npt.NDArray[float64]: """ @@ -412,6 +453,7 @@ class VariationalLearningHistory: """ :type: List[float] """ + @property def hypers(self) -> FMHyperParameters: """ diff --git a/tests/classification/test_classification.py b/tests/classification/test_classification.py index e82203c..b69e7db 100644 --- a/tests/classification/test_classification.py +++ b/tests/classification/test_classification.py @@ -1,7 +1,6 @@ -from typing import Tuple - import pickle from io import BytesIO +from typing import Tuple import numpy as np import pytest @@ -35,7 +34,7 @@ def test_middle_clf( if use_libfm_callback: np.testing.assert_allclose(fm.predict_proba(X), callback.predictions / 200) - stream = BytesIO() + stream = BytesIO() pickle.dump(fm, stream) stream.seek(0) read_fm = pickle.load(stream) @@ -62,7 +61,7 @@ def test_middle_clf( assert fm.predictor_ is not None - vfm_stream = BytesIO() + vfm_stream = BytesIO() pickle.dump(vfm, vfm_stream) vfm_stream.seek(0) read_vfm = pickle.load(vfm_stream) diff --git a/tests/conftest.py b/tests/conftest.py index 81080c8..6a213a6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,9 +15,7 @@ def stub_weight() -> FMWeights: weights = FMWeights( -3.0, np.asarray([1.0, 2.0, -1.0]), - np.asarray( - [[1.0, -1.0, 0], [0.0, 1.0, 1.0], [1.0, 1.0, 1.0], [-1.0, 0, -1.0]] - ), + np.asarray([[1.0, -1.0, 0], [0.0, 1.0, 1.0], [1.0, 1.0, 1.0], [-1.0, 0, -1.0]]), ) return weights From b73c7bbc03a7d49372a02da57cdb3391e069f0b1 Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Fri, 11 Jul 2025 18:47:15 +0900 Subject: [PATCH 09/23] pre-commit --- .github/workflows/pre-commit.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 714f374..7fd4104 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -10,4 +10,4 @@ jobs: steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v3 - - uses: pre-commit/action@v3.0.0 \ No newline at end of file + - uses: pre-commit/action@v3.0.0 From f4bb89ba76d5feef7f4802079d0c5b7e33d2b9c3 Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Fri, 11 Jul 2025 18:56:24 +0900 Subject: [PATCH 10/23] fix wheels.yml --- .github/workflows/run-test.yml | 10 +++++----- .github/workflows/wheels.yml | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/run-test.yml b/.github/workflows/run-test.yml index 6690726..721be21 100644 --- a/.github/workflows/run-test.yml +++ b/.github/workflows/run-test.yml @@ -6,20 +6,20 @@ jobs: env: OS: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: - python-version: "3.11" + python-version: '3.13' - name: Build myfm run: | pip install --upgrade pip - pip install numpy scipy pandas + pip install scikit-build setuptools-scm sudo apt-get install lcov FLAGS="-fprofile-arcs -ftest-coverage" - CFLAGS="$FLAGS" CXXFLAGS="$FLAGS" pip install -e . + CFLAGS="$FLAGS" CXXFLAGS="$FLAGS" pip install --editable . - name: Run pytest run: | pip install pytest pytest-cov pytest-mock diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index c51925b..207e174 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -1,8 +1,8 @@ name: Build wheel on: push: -# branches: -# - main + branches: + - main release: types: - created From 4eda4836b91c3ced4af4d4d8f7f80924f1bc6488 Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Fri, 11 Jul 2025 19:09:46 +0900 Subject: [PATCH 11/23] separate mypy --- .pre-commit-config.yaml | 5 - create_nb_stub.sh | 10 + examples/oprobit_example.py | 2 +- mypy.ini | 2 +- src/myfm/_myfm.pyi | 777 ++++++++++++++++------------------- src/myfm/utils/dummy_data.py | 2 +- 6 files changed, 368 insertions(+), 430 deletions(-) create mode 100644 create_nb_stub.sh diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d7a3e55..816df98 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,11 +16,6 @@ repos: hooks: - id: isort name: isort - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.13.0 - hooks: - - id: mypy - additional_dependencies: ['types-requests', 'types-setuptools'] - repo: https://github.com/psf/black rev: 24.10.0 hooks: diff --git a/create_nb_stub.sh b/create_nb_stub.sh new file mode 100644 index 0000000..d506335 --- /dev/null +++ b/create_nb_stub.sh @@ -0,0 +1,10 @@ +#!/bin/bash +modules=( \ +"myfm._myfm" +) +for module_name in "${modules[@]}" +do + echo "Create stub for $module_name" + output_path="src/$(echo "${module_name}" | sed 's/\./\//g').pyi" + python -m "nanobind.stubgen" -m "$module_name" -o "$output_path" +done diff --git a/examples/oprobit_example.py b/examples/oprobit_example.py index abab773..5fbe53a 100644 --- a/examples/oprobit_example.py +++ b/examples/oprobit_example.py @@ -27,5 +27,5 @@ n_iter=11000, n_kept_samples=10000, ) - +assert fm.cutpoint_samples is not None print(fm.cutpoint_samples.mean(axis=0)) diff --git a/mypy.ini b/mypy.ini index e66be81..2727156 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2,7 +2,7 @@ # Specify the target platform details in config, so your developers are # free to run mypy on Windows, Linux, or macOS and get consistent # results. -python_version=3.13 +python_version=3.11 platform=linux show_column_numbers=True diff --git a/src/myfm/_myfm.pyi b/src/myfm/_myfm.pyi index 101f595..9f61d21 100644 --- a/src/myfm/_myfm.pyi +++ b/src/myfm/_myfm.pyi @@ -1,218 +1,25 @@ -"""Backend C++ implementation for myfm.""" - -from typing import Iterable as iterable -from typing import Iterator as iterator -from typing import * +import enum +from collections.abc import Callable, Sequence +from typing import Annotated import scipy.sparse -from numpy import float64 -from numpy import typing as npt +from numpy.typing import ArrayLike -import myfm._myfm +class TaskType(enum.Enum): + REGRESSION = 0 -__all__ = [ - "ConfigBuilder", - "FM", - "FMHyperParameters", - "FMLearningConfig", - "FMTrainer", - "LearningHistory", - "Predictor", - "RelationBlock", - "TaskType", - "VariationalFM", - "VariationalFMHyperParameters", - "VariationalFMTrainer", - "VariationalLearningHistory", - "VariationalPredictor", - "create_train_fm", - "create_train_vfm", - "mean_var_truncated_normal_left", - "mean_var_truncated_normal_right", -] + CLASSIFICATION = 1 -class ConfigBuilder: - def __init__(self) -> None: ... - def build(self) -> FMLearningConfig: ... - def set_alpha_0(self, arg0: float) -> ConfigBuilder: ... - def set_beta_0(self, arg0: float) -> ConfigBuilder: ... - def set_cutpoint_groups( - self, arg0: List[Tuple[int, List[int]]] - ) -> ConfigBuilder: ... - def set_cutpoint_scale(self, arg0: float) -> ConfigBuilder: ... - def set_gamma_0(self, arg0: float) -> ConfigBuilder: ... - def set_group_index(self, arg0: List[int]) -> ConfigBuilder: ... - def set_identical_groups(self, arg0: int) -> ConfigBuilder: ... - def set_mu_0(self, arg0: float) -> ConfigBuilder: ... - def set_n_iter(self, arg0: int) -> ConfigBuilder: ... - def set_n_kept_samples(self, arg0: int) -> ConfigBuilder: ... - def set_nu_oprobit(self, arg0: int) -> ConfigBuilder: ... - def set_reg_0(self, arg0: float) -> ConfigBuilder: ... - def set_task_type(self, arg0: TaskType) -> ConfigBuilder: ... - pass - -class FM: - def __getstate__(self) -> tuple: ... - def __repr__(self) -> str: ... - def __setstate__(self, arg0: tuple) -> None: ... - def predict_score( - self, arg0: scipy.sparse.csr_matrix[float64], arg1: List[RelationBlock] - ) -> npt.NDArray[float64]: ... - def oprobit_predict_proba( - self, - arg0: scipy.sparse.csr_matrix[float64], - arg1: List[RelationBlock], - arg2: int, - ) -> npt.NDArray[float64]: ... - @property - def V(self) -> npt.NDArray[float64]: - """ - :type: npt.NDArray[float64] - """ - - @V.setter - def V(self, arg0: npt.NDArray[float64]) -> None: - pass - - @property - def cutpoints(self) -> List[npt.NDArray[float64]]: - """ - :type: List[npt.NDArray[float64]] - """ - - @cutpoints.setter - def cutpoints(self, arg0: List[npt.NDArray[float64]]) -> None: - pass - - @property - def w(self) -> npt.NDArray[float64]: - """ - :type: npt.NDArray[float64] - """ - - @w.setter - def w(self, arg0: npt.NDArray[float64]) -> None: - pass - - @property - def w0(self) -> float: - """ - :type: float - """ - - @w0.setter - def w0(self, arg0: float) -> None: - pass - pass - -class FMHyperParameters: - def __getstate__(self) -> tuple: ... - def __setstate__(self, arg0: tuple) -> None: ... - @property - def alpha(self) -> float: - """ - :type: float - """ - - @property - def lambda_V(self) -> npt.NDArray[float64]: - """ - :type: npt.NDArray[float64] - """ - - @property - def lambda_w(self) -> npt.NDArray[float64]: - """ - :type: npt.NDArray[float64] - """ - - @property - def mu_V(self) -> npt.NDArray[float64]: - """ - :type: npt.NDArray[float64] - """ - - @property - def mu_w(self) -> npt.NDArray[float64]: - """ - :type: npt.NDArray[float64] - """ - pass + ORDERED = 2 class FMLearningConfig: pass -class FMTrainer: - def __init__( - self, - arg0: scipy.sparse.csr_matrix[float64], - arg1: List[RelationBlock], - arg2: npt.NDArray[float64], - arg3: int, - arg4: FMLearningConfig, - ) -> None: ... - def create_FM(self, arg0: int, arg1: float) -> FM: ... - def create_Hyper(self, arg0: int) -> FMHyperParameters: ... - pass - -class LearningHistory: - def __getstate__(self) -> tuple: ... - def __setstate__(self, arg0: tuple) -> None: ... - @property - def hypers(self) -> List[FMHyperParameters]: - """ - :type: List[FMHyperParameters] - """ - - @property - def n_mh_accept(self) -> List[int]: - """ - :type: List[int] - """ - - @property - def train_log_losses(self) -> List[float]: - """ - :type: List[float] - """ - pass - -class Predictor: - def __getstate__(self) -> tuple: ... - def __setstate__(self, arg0: tuple) -> None: ... - def predict( - self, arg0: scipy.sparse.csr_matrix[float64], arg1: List[RelationBlock] - ) -> npt.NDArray[float64]: ... - def predict_parallel( - self, - arg0: scipy.sparse.csr_matrix[float64], - arg1: List[RelationBlock], - arg2: int, - ) -> npt.NDArray[float64]: ... - def predict_parallel_oprobit( - self, - arg0: scipy.sparse.csr_matrix[float64], - arg1: List[RelationBlock], - arg2: int, - arg3: int, - ) -> npt.NDArray[float64]: ... - @property - def samples(self) -> List[FM]: - """ - :type: List[FM] - """ - pass - class RelationBlock: - """ - The RelationBlock Class. - """ + """The RelationBlock Class.""" - def __getstate__(self) -> tuple: ... def __init__( - self, - original_to_block: List[int], - data: scipy.sparse.csr_matrix[float64], + self, original_to_block: Sequence[int], data: scipy.sparse.csr_matrix[float] ) -> None: """ Initializes relation block. @@ -230,284 +37,410 @@ class RelationBlock: The entries of `original_to_block` must be in the [0, data.shape[0]-1]. """ - def __repr__(self) -> str: ... - def __setstate__(self, arg0: tuple) -> None: ... @property - def block_size(self) -> int: - """ - :type: int - """ - + def original_to_block(self) -> list[int]: ... @property - def data(self) -> scipy.sparse.csr_matrix[float64]: - """ - :type: scipy.sparse.csr_matrix[float64] - """ - + def data(self) -> scipy.sparse.csr_matrix[float]: ... @property - def feature_size(self) -> int: - """ - :type: int - """ - + def mapper_size(self) -> int: ... @property - def mapper_size(self) -> int: - """ - :type: int - """ - + def block_size(self) -> int: ... @property - def original_to_block(self) -> List[int]: - """ - :type: List[int] - """ - pass - -class TaskType: - """ - Members: - - REGRESSION - - CLASSIFICATION + def feature_size(self) -> int: ... + def __repr__(self) -> str: ... + def __getstate__(self) -> tuple[list[int], scipy.sparse.csr_matrix[float]]: ... + def __setstate__( + self, arg: tuple[Sequence[int], scipy.sparse.csr_matrix[float]], / + ) -> None: ... - ORDERED - """ +class ConfigBuilder: + def __init__(self) -> None: ... + def set_alpha_0(self, arg: float, /) -> None: ... + def set_beta_0(self, arg: float, /) -> None: ... + def set_gamma_0(self, arg: float, /) -> None: ... + def set_mu_0(self, arg: float, /) -> None: ... + def set_reg_0(self, arg: float, /) -> None: ... + def set_n_iter(self, arg: int, /) -> None: ... + def set_n_kept_samples(self, arg: int, /) -> None: ... + def set_task_type(self, arg: TaskType, /) -> None: ... + def set_nu_oprobit(self, arg: int, /) -> None: ... + def set_fit_w0(self, arg: bool, /) -> None: ... + def set_fit_linear(self, arg: bool, /) -> None: ... + def set_group_index(self, arg: Sequence[int], /) -> None: ... + def set_identical_groups(self, arg: int, /) -> None: ... + def set_cutpoint_scale(self, arg: float, /) -> None: ... + def set_cutpoint_groups( + self, arg: Sequence[tuple[int, Sequence[int]]], / + ) -> None: ... + def build(self) -> FMLearningConfig: ... - def __init__(self, arg0: int) -> None: ... - def __int__(self) -> int: ... +class FM: @property - def name(self) -> str: - """ - (self: handle) -> str - - :type: str - """ - CLASSIFICATION: myfm._myfm.TaskType # value = TaskType.CLASSIFICATION - ORDERED: myfm._myfm.TaskType # value = TaskType.ORDERED - REGRESSION: myfm._myfm.TaskType # value = TaskType.REGRESSION - __entries: dict # value = {'REGRESSION': (TaskType.REGRESSION, None), 'CLASSIFICATION': (TaskType.CLASSIFICATION, None), 'ORDERED': (TaskType.ORDERED, None)} - __members__: dict # value = {'REGRESSION': TaskType.REGRESSION, 'CLASSIFICATION': TaskType.CLASSIFICATION, 'ORDERED': TaskType.ORDERED} - pass + def w0(self) -> float: ... + @w0.setter + def w0(self, arg: float, /) -> None: ... + @property + def w( + self, + ) -> Annotated[ArrayLike, dict(dtype="float64", shape=(None), order="C")]: ... + @w.setter + def w( + self, + arg: Annotated[ArrayLike, dict(dtype="float64", shape=(None), order="C")], + /, + ) -> None: ... + @property + def V( + self, + ) -> Annotated[ArrayLike, dict(dtype="float64", shape=(None, None), order="F")]: ... + @V.setter + def V( + self, + arg: Annotated[ArrayLike, dict(dtype="float64", shape=(None, None), order="F")], + /, + ) -> None: ... + @property + def cutpoints( + self, + ) -> list[Annotated[ArrayLike, dict(dtype="float64", shape=(None), order="C")]]: ... + @cutpoints.setter + def cutpoints( + self, + arg: Sequence[ + Annotated[ArrayLike, dict(dtype="float64", shape=(None), order="C")] + ], + /, + ) -> None: ... + def predict_score( + self, arg0: scipy.sparse.csr_matrix[float], arg1: Sequence[RelationBlock], / + ) -> Annotated[ArrayLike, dict(dtype="float64", shape=(None), order="C")]: ... + def oprobit_predict_proba( + self, + arg0: scipy.sparse.csr_matrix[float], + arg1: Sequence[RelationBlock], + arg2: int, + /, + ) -> Annotated[ArrayLike, dict(dtype="float64", shape=(None, None), order="F")]: ... + def __repr__(self) -> str: ... + def __getstate__( + self, + ) -> tuple[ + float, + Annotated[ArrayLike, dict(dtype="float64", shape=(None), order="C")], + Annotated[ArrayLike, dict(dtype="float64", shape=(None, None), order="F")], + list[Annotated[ArrayLike, dict(dtype="float64", shape=(None), order="C")]], + ]: ... + def __setstate__( + self, + arg: tuple[ + float, + Annotated[ArrayLike, dict(dtype="float64", shape=(None), order="C")], + Annotated[ArrayLike, dict(dtype="float64", shape=(None, None), order="F")], + Sequence[ + Annotated[ArrayLike, dict(dtype="float64", shape=(None), order="C")] + ], + ], + /, + ) -> None: ... class VariationalFM: - def __getstate__(self) -> tuple: ... - def __repr__(self) -> str: ... - def __setstate__(self, arg0: tuple) -> None: ... - def predict_score( - self, arg0: scipy.sparse.csr_matrix[float64], arg1: List[RelationBlock] - ) -> npt.NDArray[float64]: ... @property - def V(self) -> npt.NDArray[float64]: - """ - :type: npt.NDArray[float64] - """ - + def w0(self) -> float: ... + @w0.setter + def w0(self, arg: float, /) -> None: ... + @property + def w0_var(self) -> float: ... + @w0_var.setter + def w0_var(self, arg: float, /) -> None: ... + @property + def w( + self, + ) -> Annotated[ArrayLike, dict(dtype="float64", shape=(None), order="C")]: ... + @w.setter + def w( + self, + arg: Annotated[ArrayLike, dict(dtype="float64", shape=(None), order="C")], + /, + ) -> None: ... + @property + def w_var( + self, + ) -> Annotated[ArrayLike, dict(dtype="float64", shape=(None), order="C")]: ... + @w_var.setter + def w_var( + self, + arg: Annotated[ArrayLike, dict(dtype="float64", shape=(None), order="C")], + /, + ) -> None: ... + @property + def V( + self, + ) -> Annotated[ArrayLike, dict(dtype="float64", shape=(None, None), order="F")]: ... @V.setter - def V(self, arg0: npt.NDArray[float64]) -> None: - pass - + def V( + self, + arg: Annotated[ArrayLike, dict(dtype="float64", shape=(None, None), order="F")], + /, + ) -> None: ... @property - def V_var(self) -> npt.NDArray[float64]: - """ - :type: npt.NDArray[float64] - """ - + def V_var( + self, + ) -> Annotated[ArrayLike, dict(dtype="float64", shape=(None, None), order="F")]: ... @V_var.setter - def V_var(self, arg0: npt.NDArray[float64]) -> None: - pass - + def V_var( + self, + arg: Annotated[ArrayLike, dict(dtype="float64", shape=(None, None), order="F")], + /, + ) -> None: ... @property - def cutpoints(self) -> List[npt.NDArray[float64]]: - """ - :type: List[npt.NDArray[float64]] - """ - + def cutpoints( + self, + ) -> list[Annotated[ArrayLike, dict(dtype="float64", shape=(None), order="C")]]: ... @cutpoints.setter - def cutpoints(self, arg0: List[npt.NDArray[float64]]) -> None: - pass + def cutpoints( + self, + arg: Sequence[ + Annotated[ArrayLike, dict(dtype="float64", shape=(None), order="C")] + ], + /, + ) -> None: ... + def predict_score( + self, arg0: scipy.sparse.csr_matrix[float], arg1: Sequence[RelationBlock], / + ) -> Annotated[ArrayLike, dict(dtype="float64", shape=(None), order="C")]: ... + def __repr__(self) -> str: ... + def __getstate__( + self, + ) -> tuple[ + float, + float, + Annotated[ArrayLike, dict(dtype="float64", shape=(None), order="C")], + Annotated[ArrayLike, dict(dtype="float64", shape=(None), order="C")], + Annotated[ArrayLike, dict(dtype="float64", shape=(None, None), order="F")], + Annotated[ArrayLike, dict(dtype="float64", shape=(None, None), order="F")], + list[Annotated[ArrayLike, dict(dtype="float64", shape=(None), order="C")]], + ]: ... + def __setstate__( + self, + arg: tuple[ + float, + float, + Annotated[ArrayLike, dict(dtype="float64", shape=(None), order="C")], + Annotated[ArrayLike, dict(dtype="float64", shape=(None), order="C")], + Annotated[ArrayLike, dict(dtype="float64", shape=(None, None), order="F")], + Annotated[ArrayLike, dict(dtype="float64", shape=(None, None), order="F")], + Sequence[ + Annotated[ArrayLike, dict(dtype="float64", shape=(None), order="C")] + ], + ], + /, + ) -> None: ... +class FMHyperParameters: @property - def w(self) -> npt.NDArray[float64]: - """ - :type: npt.NDArray[float64] - """ - - @w.setter - def w(self, arg0: npt.NDArray[float64]) -> None: - pass - + def alpha(self) -> float: ... @property - def w0(self) -> float: - """ - :type: float - """ - - @w0.setter - def w0(self, arg0: float) -> None: - pass - + def mu_w( + self, + ) -> Annotated[ArrayLike, dict(dtype="float64", shape=(None), order="C")]: ... @property - def w0_var(self) -> float: - """ - :type: float - """ - - @w0_var.setter - def w0_var(self, arg0: float) -> None: - pass - + def lambda_w( + self, + ) -> Annotated[ArrayLike, dict(dtype="float64", shape=(None), order="C")]: ... @property - def w_var(self) -> npt.NDArray[float64]: - """ - :type: npt.NDArray[float64] - """ - - @w_var.setter - def w_var(self, arg0: npt.NDArray[float64]) -> None: - pass - pass + def mu_V( + self, + ) -> Annotated[ArrayLike, dict(dtype="float64", shape=(None, None), order="F")]: ... + @property + def lambda_V( + self, + ) -> Annotated[ArrayLike, dict(dtype="float64", shape=(None, None), order="F")]: ... + def __getstate__( + self, + ) -> tuple[ + float, + Annotated[ArrayLike, dict(dtype="float64", shape=(None), order="C")], + Annotated[ArrayLike, dict(dtype="float64", shape=(None), order="C")], + Annotated[ArrayLike, dict(dtype="float64", shape=(None, None), order="F")], + Annotated[ArrayLike, dict(dtype="float64", shape=(None, None), order="F")], + ]: ... + def __setstate__( + self, + arg: tuple[ + float, + Annotated[ArrayLike, dict(dtype="float64", shape=(None), order="C")], + Annotated[ArrayLike, dict(dtype="float64", shape=(None), order="C")], + Annotated[ArrayLike, dict(dtype="float64", shape=(None, None), order="F")], + Annotated[ArrayLike, dict(dtype="float64", shape=(None, None), order="F")], + ], + /, + ) -> None: ... class VariationalFMHyperParameters: - def __getstate__(self) -> tuple: ... - def __setstate__(self, arg0: tuple) -> None: ... @property - def alpha(self) -> float: - """ - :type: float - """ - + def alpha(self) -> float: ... @property - def alpha_rate(self) -> float: - """ - :type: float - """ - + def alpha_rate(self) -> float: ... @property - def lambda_V(self) -> npt.NDArray[float64]: - """ - :type: npt.NDArray[float64] - """ - + def mu_w( + self, + ) -> Annotated[ArrayLike, dict(dtype="float64", shape=(None), order="C")]: ... @property - def lambda_V_rate(self) -> npt.NDArray[float64]: - """ - :type: npt.NDArray[float64] - """ - + def mu_w_var( + self, + ) -> Annotated[ArrayLike, dict(dtype="float64", shape=(None), order="C")]: ... @property - def lambda_w(self) -> npt.NDArray[float64]: - """ - :type: npt.NDArray[float64] - """ - + def lambda_w( + self, + ) -> Annotated[ArrayLike, dict(dtype="float64", shape=(None), order="C")]: ... @property - def lambda_w_rate(self) -> npt.NDArray[float64]: - """ - :type: npt.NDArray[float64] - """ - + def lambda_w_rate( + self, + ) -> Annotated[ArrayLike, dict(dtype="float64", shape=(None), order="C")]: ... @property - def mu_V(self) -> npt.NDArray[float64]: - """ - :type: npt.NDArray[float64] - """ - + def mu_V( + self, + ) -> Annotated[ArrayLike, dict(dtype="float64", shape=(None, None), order="F")]: ... @property - def mu_V_var(self) -> npt.NDArray[float64]: - """ - :type: npt.NDArray[float64] - """ - + def mu_V_var( + self, + ) -> Annotated[ArrayLike, dict(dtype="float64", shape=(None, None), order="F")]: ... @property - def mu_w(self) -> npt.NDArray[float64]: - """ - :type: npt.NDArray[float64] - """ + def lambda_V( + self, + ) -> Annotated[ArrayLike, dict(dtype="float64", shape=(None, None), order="F")]: ... + @property + def lambda_V_rate( + self, + ) -> Annotated[ArrayLike, dict(dtype="float64", shape=(None, None), order="F")]: ... + def __getstate__(self) -> tuple: ... + def __setstate__( + self, + arg: tuple[ + float, + float, + Annotated[ArrayLike, dict(dtype="float64", shape=(None), order="C")], + Annotated[ArrayLike, dict(dtype="float64", shape=(None), order="C")], + Annotated[ArrayLike, dict(dtype="float64", shape=(None), order="C")], + Annotated[ArrayLike, dict(dtype="float64", shape=(None), order="C")], + Annotated[ArrayLike, dict(dtype="float64", shape=(None, None), order="F")], + Annotated[ArrayLike, dict(dtype="float64", shape=(None, None), order="F")], + Annotated[ArrayLike, dict(dtype="float64", shape=(None, None), order="F")], + Annotated[ArrayLike, dict(dtype="float64", shape=(None, None), order="F")], + ], + /, + ) -> None: ... +class Predictor: @property - def mu_w_var(self) -> npt.NDArray[float64]: - """ - :type: npt.NDArray[float64] - """ - pass + def samples(self) -> list[FM]: ... + def predict( + self, arg0: scipy.sparse.csr_matrix[float], arg1: Sequence[RelationBlock], / + ) -> Annotated[ArrayLike, dict(dtype="float64", shape=(None), order="C")]: ... + def predict_parallel( + self, + arg0: scipy.sparse.csr_matrix[float], + arg1: Sequence[RelationBlock], + arg2: int, + /, + ) -> Annotated[ArrayLike, dict(dtype="float64", shape=(None), order="C")]: ... + def predict_parallel_oprobit( + self, + arg0: scipy.sparse.csr_matrix[float], + arg1: Sequence[RelationBlock], + arg2: int, + arg3: int, + /, + ) -> Annotated[ArrayLike, dict(dtype="float64", shape=(None, None), order="F")]: ... + def __getstate__(self) -> tuple[int, int, int, list[FM]]: ... + def __setstate__(self, arg: tuple[int, int, TaskType, Sequence[FM]], /) -> None: ... + +class VariationalPredictor: + def predict( + self, arg0: scipy.sparse.csr_matrix[float], arg1: Sequence[RelationBlock], / + ) -> Annotated[ArrayLike, dict(dtype="float64", shape=(None), order="C")]: ... + def weights(self) -> VariationalFM: ... + def __getstate__(self) -> tuple[int, int, int, list[VariationalFM]]: ... + def __setstate__( + self, arg: tuple[int, int, TaskType, Sequence[VariationalFM]], / + ) -> None: ... + +class FMTrainer: + def __init__( + self, + arg0: scipy.sparse.csr_matrix[float], + arg1: Sequence[RelationBlock], + arg2: Annotated[ArrayLike, dict(dtype="float64", shape=(None), order="C")], + arg3: int, + arg4: FMLearningConfig, + /, + ) -> None: ... + def create_FM(self, arg0: int, arg1: float, /) -> FM: ... + def create_Hyper(self, arg: int, /) -> FMHyperParameters: ... class VariationalFMTrainer: def __init__( self, - arg0: scipy.sparse.csr_matrix[float64], - arg1: List[RelationBlock], - arg2: npt.NDArray[float64], + arg0: scipy.sparse.csr_matrix[float], + arg1: Sequence[RelationBlock], + arg2: Annotated[ArrayLike, dict(dtype="float64", shape=(None), order="C")], arg3: int, arg4: FMLearningConfig, + /, ) -> None: ... - def create_FM(self, arg0: int, arg1: float) -> VariationalFM: ... - def create_Hyper(self, arg0: int) -> VariationalFMHyperParameters: ... - pass + def create_FM(self, arg0: int, arg1: float, /) -> VariationalFM: ... + def create_Hyper(self, arg: int, /) -> VariationalFMHyperParameters: ... -class VariationalLearningHistory: - def __getstate__(self) -> tuple: ... - def __setstate__(self, arg0: tuple) -> None: ... +class LearningHistory: @property - def elbos(self) -> List[float]: - """ - :type: List[float] - """ - + def hypers(self) -> list[FMHyperParameters]: ... @property - def hypers(self) -> FMHyperParameters: - """ - :type: FMHyperParameters - """ - pass + def train_log_losses(self) -> list[float]: ... + @property + def n_mh_accept(self) -> list[int]: ... + def __getstate__( + self, + ) -> tuple[list[FMHyperParameters], list[float], list[int]]: ... + def __setstate__( + self, arg: tuple[Sequence[FMHyperParameters], Sequence[float], Sequence[int]], / + ) -> None: ... -class VariationalPredictor: - def __getstate__(self) -> tuple: ... - def __setstate__(self, arg0: tuple) -> None: ... - def predict( - self, arg0: scipy.sparse.csr_matrix[float64], arg1: List[RelationBlock] - ) -> npt.NDArray[float64]: ... - def weights(self) -> VariationalFM: ... - pass +class VariationalLearningHistory: + @property + def hypers(self) -> FMHyperParameters: ... + @property + def elbos(self) -> list[float]: ... + def __getstate__(self) -> tuple[FMHyperParameters, list[float]]: ... + def __setstate__( + self, arg: tuple[FMHyperParameters, Sequence[float]], / + ) -> None: ... def create_train_fm( arg0: int, arg1: float, - arg2: scipy.sparse.csr_matrix[float64], - arg3: List[RelationBlock], - arg4: npt.NDArray[float64], + arg2: scipy.sparse.csr_matrix[float], + arg3: Sequence[RelationBlock], + arg4: Annotated[ArrayLike, dict(dtype="float64", shape=(None), order="C")], arg5: int, arg6: FMLearningConfig, arg7: Callable[[int, FM, FMHyperParameters, LearningHistory], bool], -) -> Tuple[Predictor, LearningHistory]: - """ - create and train fm. - """ + /, +) -> tuple[Predictor, LearningHistory]: + """create and train fm.""" def create_train_vfm( rank: int, init_std: float, - X: scipy.sparse.csr_matrix[float64], - relations: List[RelationBlock], - y: npt.NDArray[float64], + X: scipy.sparse.csr_matrix[float], + relations: Sequence[RelationBlock], + y: Annotated[ArrayLike, dict(dtype="float64", shape=(None), order="C")], random_seed: int, learning_config: FMLearningConfig, callback: Callable[ - [ - int, - VariationalFM, - VariationalFMHyperParameters, - VariationalLearningHistory, - ], + [int, VariationalFM, VariationalFMHyperParameters, VariationalLearningHistory], bool, ], -) -> Tuple[VariationalPredictor, VariationalLearningHistory]: - """ - create and train fm. - """ +) -> tuple[VariationalPredictor, VariationalLearningHistory]: + """create and train fm.""" -def mean_var_truncated_normal_left(arg0: float) -> Tuple[float, float, float]: - pass - -def mean_var_truncated_normal_right(arg0: float) -> Tuple[float, float, float]: - pass +def mean_var_truncated_normal_left(arg: float, /) -> tuple[float, float, float]: ... +def mean_var_truncated_normal_right(arg: float, /) -> tuple[float, float, float]: ... diff --git a/src/myfm/utils/dummy_data.py b/src/myfm/utils/dummy_data.py index f2ef1e5..27720b8 100644 --- a/src/myfm/utils/dummy_data.py +++ b/src/myfm/utils/dummy_data.py @@ -39,7 +39,7 @@ def gen_dummy_rating_df( user_factor[result_df[user_colname].values - 1, :] * item_factor[result_df[item_colname].values - 1, :] ).sum(axis=1) - cutpoints: List[float] = list(np.percentile(score, [20, 40, 60, 80])) # type: ignore + cutpoints: List[float] = list(np.percentile(score, [20, 40, 60, 80])) rating = np.ones((size,), dtype=np.int64) for cp in cutpoints: rating += score >= cp From f8ce850fa8c687e1c40890bf66e620482dd77c46 Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Fri, 11 Jul 2025 20:52:06 +0900 Subject: [PATCH 12/23] coverage order --- .github/workflows/run-test.yml | 11 +++-------- .gitignore | 2 ++ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/workflows/run-test.yml b/.github/workflows/run-test.yml index 721be21..b91dd55 100644 --- a/.github/workflows/run-test.yml +++ b/.github/workflows/run-test.yml @@ -13,19 +13,14 @@ jobs: uses: actions/setup-python@v5 with: python-version: '3.13' - - name: Build myfm + - name: Build myfm & test run: | - pip install --upgrade pip - pip install scikit-build setuptools-scm sudo apt-get install lcov + pip install --upgrade pip + pip install scikit-build setuptools-scm pytest pytest-cov pytest-mock nanobind FLAGS="-fprofile-arcs -ftest-coverage" CFLAGS="$FLAGS" CXXFLAGS="$FLAGS" pip install --editable . - - name: Run pytest - run: | - pip install pytest pytest-cov pytest-mock pytest --cov=./src/myfm tests/ - - name: Generate coverage (ubuntu) - run: | coverage xml lcov -d `pwd` -c -o coverage.info - name: Upload coverage to Codecov diff --git a/.gitignore b/.gitignore index d0f8494..cebc871 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,5 @@ doc/source/api_reference/*.rst src/myfm/_version.py mise.toml + +.venv \ No newline at end of file From 51bda04bb32c434a6d308fe7fb4070ee0c559fa9 Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Fri, 11 Jul 2025 20:57:06 +0900 Subject: [PATCH 13/23] O0 flags --- .github/workflows/run-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-test.yml b/.github/workflows/run-test.yml index b91dd55..a77c412 100644 --- a/.github/workflows/run-test.yml +++ b/.github/workflows/run-test.yml @@ -18,8 +18,8 @@ jobs: sudo apt-get install lcov pip install --upgrade pip pip install scikit-build setuptools-scm pytest pytest-cov pytest-mock nanobind - FLAGS="-fprofile-arcs -ftest-coverage" - CFLAGS="$FLAGS" CXXFLAGS="$FLAGS" pip install --editable . + FLAGS="-O0 -fprofile-arcs -ftest-coverage -fprofile-update=atomic" + CFLAGS="$FLAGS" CXX_FLAGS="$FLAGS" pip install --editable . pytest --cov=./src/myfm tests/ coverage xml lcov -d `pwd` -c -o coverage.info From f0c014de091783e68062d05fe1d8ec31069ac42a Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Fri, 11 Jul 2025 21:15:46 +0900 Subject: [PATCH 14/23] CXXFLAGS --- .github/workflows/run-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-test.yml b/.github/workflows/run-test.yml index a77c412..b732762 100644 --- a/.github/workflows/run-test.yml +++ b/.github/workflows/run-test.yml @@ -19,7 +19,7 @@ jobs: pip install --upgrade pip pip install scikit-build setuptools-scm pytest pytest-cov pytest-mock nanobind FLAGS="-O0 -fprofile-arcs -ftest-coverage -fprofile-update=atomic" - CFLAGS="$FLAGS" CXX_FLAGS="$FLAGS" pip install --editable . + CFLAGS="$FLAGS" CXXFLAGS="$FLAGS" pip install --editable . pytest --cov=./src/myfm tests/ coverage xml lcov -d `pwd` -c -o coverage.info From b11bdd8b188d4c5185057ab049a89db79d0eeab9 Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Fri, 11 Jul 2025 21:25:01 +0900 Subject: [PATCH 15/23] --ignore-errors --- .github/workflows/run-test.yml | 2 +- .gitignore | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-test.yml b/.github/workflows/run-test.yml index b732762..4cd5e76 100644 --- a/.github/workflows/run-test.yml +++ b/.github/workflows/run-test.yml @@ -22,7 +22,7 @@ jobs: CFLAGS="$FLAGS" CXXFLAGS="$FLAGS" pip install --editable . pytest --cov=./src/myfm tests/ coverage xml - lcov -d `pwd` -c -o coverage.info + lcov -d `pwd` -c -o coverage.info -ignore-errors - name: Upload coverage to Codecov uses: codecov/codecov-action@v1 with: diff --git a/.gitignore b/.gitignore index cebc871..1032915 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,4 @@ src/myfm/_version.py mise.toml -.venv \ No newline at end of file +.venv From 4104b63ceb29be806d2305393e482395eb6501d0 Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Fri, 11 Jul 2025 21:27:54 +0900 Subject: [PATCH 16/23] source --- .github/workflows/run-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-test.yml b/.github/workflows/run-test.yml index 4cd5e76..739bab7 100644 --- a/.github/workflows/run-test.yml +++ b/.github/workflows/run-test.yml @@ -22,7 +22,7 @@ jobs: CFLAGS="$FLAGS" CXXFLAGS="$FLAGS" pip install --editable . pytest --cov=./src/myfm tests/ coverage xml - lcov -d `pwd` -c -o coverage.info -ignore-errors + lcov -d `pwd` -c -o coverage.info --ignore-errors source - name: Upload coverage to Codecov uses: codecov/codecov-action@v1 with: From 701ee44e7944d92d85dc389620f21145e9522486 Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Fri, 11 Jul 2025 21:33:25 +0900 Subject: [PATCH 17/23] oidc --- .github/workflows/run-test.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-test.yml b/.github/workflows/run-test.yml index 739bab7..d20f700 100644 --- a/.github/workflows/run-test.yml +++ b/.github/workflows/run-test.yml @@ -2,6 +2,8 @@ name: Test & Upload coverage on: [push] jobs: run_pytest_upload_coverage: + permissions: + id-token: write runs-on: ubuntu-latest env: OS: ubuntu-latest @@ -24,10 +26,11 @@ jobs: coverage xml lcov -d `pwd` -c -o coverage.info --ignore-errors source - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v5 with: + use_oidc: true files: ./coverage.xml,./coverage.info - verbose: false + verbose: true env_vars: OS,PYTHON name: codecov-umbrella fail_ci_if_error: false From 21b41e03a116bd365332e4388952a027fa256457 Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Fri, 11 Jul 2025 21:38:27 +0900 Subject: [PATCH 18/23] doctest --- .github/workflows/doctest.yml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/doctest.yml b/.github/workflows/doctest.yml index a2a4303..7127cd9 100644 --- a/.github/workflows/doctest.yml +++ b/.github/workflows/doctest.yml @@ -7,16 +7,13 @@ jobs: OS: ubuntu-latest steps: - uses: actions/checkout@v3 - with: - fetch-depth: 0 - name: Setup Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: - python-version: "3.11" + python-version: "3.13" - name: Build myfm run: | - pip install --upgrade pip - pip install numpy scipy pandas scikit-learn + pip install --upgrade pip scikit-build setuptools-scm nanobind pip install . curl http://files.grouplens.org/datasets/movielens/ml-100k.zip -o ~/.ml-100k.zip - name: Run pytest From c5d85fae0e904ea0a7c95fe2c3495ec4f91bbdec Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Fri, 11 Jul 2025 21:41:44 +0900 Subject: [PATCH 19/23] sklearn --- .github/workflows/doctest.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/doctest.yml b/.github/workflows/doctest.yml index 7127cd9..c24f58d 100644 --- a/.github/workflows/doctest.yml +++ b/.github/workflows/doctest.yml @@ -6,7 +6,7 @@ jobs: env: OS: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v5 with: @@ -16,9 +16,9 @@ jobs: pip install --upgrade pip scikit-build setuptools-scm nanobind pip install . curl http://files.grouplens.org/datasets/movielens/ml-100k.zip -o ~/.ml-100k.zip - - name: Run pytest + - name: install pytest run: | - pip install pytest phmdoctest sphinx==4.4.0 sphinx_rtd_theme + pip install pytest phmdoctest sphinx==4.4.0 sphinx_rtd_theme sklearn - name: Test Readme.md run: | GEN_TEST_FILE=phmdoctest_out.py From 3c32317dd2a57df90d293d7daa406bc07242fe8d Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Fri, 11 Jul 2025 21:43:17 +0900 Subject: [PATCH 20/23] scikit-learn --- .github/workflows/doctest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/doctest.yml b/.github/workflows/doctest.yml index c24f58d..d636b59 100644 --- a/.github/workflows/doctest.yml +++ b/.github/workflows/doctest.yml @@ -18,7 +18,7 @@ jobs: curl http://files.grouplens.org/datasets/movielens/ml-100k.zip -o ~/.ml-100k.zip - name: install pytest run: | - pip install pytest phmdoctest sphinx==4.4.0 sphinx_rtd_theme sklearn + pip install pytest phmdoctest sphinx==4.4.0 sphinx_rtd_theme scikit-learn - name: Test Readme.md run: | GEN_TEST_FILE=phmdoctest_out.py From 3fd89091302a42fac750d4ca23bafc2b96fd257a Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Fri, 11 Jul 2025 22:45:39 +0900 Subject: [PATCH 21/23] doc --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ec8a297..3fd9ef8 100644 --- a/README.md +++ b/README.md @@ -169,7 +169,7 @@ X_movie = movie_ohe.transform( block_user = RelationBlock(user_indices, X_user) block_movie = RelationBlock(movie_indices, X_movie) -fm = MyFMRegressor(rank=2).fit(None, ratings.rating, X_rel=[block_user, block_movie]) +fm = MyFMRegressor(rank=2).fit(None, ratings.rating.values, X_rel=[block_user, block_movie]) prediction_df = pd.DataFrame([ dict(user_id=user_id,movie_id=movie_id, From 79f969e5b572de3e2dd587fb61a3dbab84869114 Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Fri, 11 Jul 2025 22:53:43 +0900 Subject: [PATCH 22/23] doctest --- .github/workflows/doctest.yml | 2 +- doc/source/relation-blocks.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/doctest.yml b/.github/workflows/doctest.yml index d636b59..4f28ebd 100644 --- a/.github/workflows/doctest.yml +++ b/.github/workflows/doctest.yml @@ -18,7 +18,7 @@ jobs: curl http://files.grouplens.org/datasets/movielens/ml-100k.zip -o ~/.ml-100k.zip - name: install pytest run: | - pip install pytest phmdoctest sphinx==4.4.0 sphinx_rtd_theme scikit-learn + pip install pytest phmdoctest sphinx==8.3.2 sphinx_rtd_theme scikit-learn - name: Test Readme.md run: | GEN_TEST_FILE=phmdoctest_out.py diff --git a/doc/source/relation-blocks.rst b/doc/source/relation-blocks.rst index 2737f52..36a2010 100644 --- a/doc/source/relation-blocks.rst +++ b/doc/source/relation-blocks.rst @@ -156,7 +156,7 @@ We now setup the problem in a non-relational way: movie_data_test[test_mid_index] ]) - fm_naive = myfm.MyFMRegressor(rank=10).fit(X_train_naive, df_train.rating, n_iter=3, n_kept_samples=3) + fm_naive = myfm.MyFMRegressor(rank=10).fit(X_train_naive, df_train.rating.values, n_iter=3, n_kept_samples=3) In my environment, it takes ~ 2s per iteration, which is much slower than pure MF example. From e6e27e36fc20822ab9247822800792a078c54050 Mon Sep 17 00:00:00 2001 From: Tomoki Ohtsuki Date: Fri, 11 Jul 2025 22:55:35 +0900 Subject: [PATCH 23/23] sphinx version --- .github/workflows/doctest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/doctest.yml b/.github/workflows/doctest.yml index 4f28ebd..39f63d6 100644 --- a/.github/workflows/doctest.yml +++ b/.github/workflows/doctest.yml @@ -18,7 +18,7 @@ jobs: curl http://files.grouplens.org/datasets/movielens/ml-100k.zip -o ~/.ml-100k.zip - name: install pytest run: | - pip install pytest phmdoctest sphinx==8.3.2 sphinx_rtd_theme scikit-learn + pip install pytest phmdoctest sphinx==8.2.3 sphinx_rtd_theme scikit-learn - name: Test Readme.md run: | GEN_TEST_FILE=phmdoctest_out.py