From d3b3151585bbbd8d5703d1a576a6abe98c57d714 Mon Sep 17 00:00:00 2001 From: Thibault Vatter Date: Sun, 9 Feb 2025 13:06:46 +0100 Subject: [PATCH] Version 0.7.1 (#172) * Add pickling feature (#168) * Add pickling feature * Small typo * modify tests * Make pickling safer with new FitControlsConfig * bump version number * upgrade nanobind (#171) * upgrade nanobind * unit test * upgrade changelog * correct unit tests * Upgrade to vinecopulib 0.7.1 (#173) * Remove codacy (#174) * Update changelog --- CHANGELOG.md | 29 ++++++ README.md | 3 +- docs/index.rst | 4 - lib/vinecopulib | 2 +- pyproject.toml | 4 +- scripts/process_changelog.py | 40 ++++---- src/include/bicop/class.hpp | 21 +++- src/include/bicop/family.hpp | 3 - src/include/bicop/fit_controls.hpp | 48 +++++++++- src/include/vinecop/class.hpp | 18 +++- src/include/vinecop/fit_controls.hpp | 78 ++++++++++++++- src/include/vinecop/rvine_structure.hpp | 13 ++- tests/__init__.py | 0 tests/helpers.py | 52 ++++++++++ tests/test_bicop.py | 7 +- tests/test_pickling.py | 121 ++++++++++++++++++++++++ tests/test_vinecop.py | 51 +++++++--- 17 files changed, 436 insertions(+), 58 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/helpers.py create mode 100644 tests/test_pickling.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 0aef566c..b0bccb18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,34 @@ # Changelog +## 0.7.1 + +## Bug fixes in `pyvinecopulib` + +* Upgrade nanobind to allow for single row matrices (fix #169 and #170) + +## New features in `pyvinecopulib` + +* Add pickle support for all classes (#168) +* Add `allow_rotation` option to `FitControlsBicop` and `FitControlsVinecop` (#168) + +## Changes in `vinecopulib` version 0.7.1 + +These changes originate from the latest release of [`vinecopulib`](https://github.com/vinecopulib/vinecopulib/releases/tag/v0.7.1), the C++ library which powers `pyvinecopulib`. + +### NEW FEATURES + +* add `allow_rotation` option to `FitControlsBicop` and `FitControlsVinecop` + to allow for the rotation of the pair copulas ([#628](https://github.com/vinecopulib/vinecopulib/pull/628)). +* add a `FitControlsConfig` struct to create flexible and yet safe constructors + for `FitControlsBicop` and `FitControlsVinecop` ([#629](https://github.com/vinecopulib/vinecopulib/pull/629)). + +### BUG FIXES + +* restrict parameter range for fitting Tawn copulas; fix handling of their + shape/argument order ([#620](https://github.com/vinecopulib/vinecopulib/pull/620)). +* compute and save loglik/nobs in `Vinecop::fit()` ([#623](https://github.com/vinecopulib/vinecopulib/pull/623)) +* disable unwanted compiler output related to BOOST_CONCEPT checks ([#624](https://github.com/vinecopulib/vinecopulib/pull/624)) + ## 0.7.0 This version introduces a switch to nanobind as a backend (#160): i.e., the C++ bindings, now use [nanobind](https://nanobind.readthedocs.io/) instead of [pybind11](https://pybind11.readthedocs.io/). diff --git a/README.md b/README.md index 682839a4..20b30f3d 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ [![Documentation](https://img.shields.io/website/http/vinecopulib.github.io/pyvinecopulib.svg)](https://vinecopulib.github.io/pyvinecopulib/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Build Status](https://github.com/vinecopulib/pyvinecopulib/actions/workflows/pypi.yml/badge.svg)](https://github.com/vinecopulib/pyvinecopulib/actions/workflows/pypi.yml) -[![Codacy Badge](https://api.codacy.com/project/badge/Grade/3c0056d3ca5244a5ba6a2b32f87be4cf)](https://www.codacy.com/gh/vinecopulib/pyvinecopulib?utm_source=github.com&utm_medium=referral&utm_content=vinecopulib/pyvinecopulib&utm_campaign=Badge_Grade) [![DOI](https://zenodo.org/badge/196999069.svg)](https://zenodo.org/badge/latestdoi/196999069) ## Introduction @@ -71,7 +70,7 @@ mamba install conda-forge::pyvinecopulib The main build time prerequisites are: * scikit-build-core (>=0.4.3), -* nanobind (>=1.3.2), +* nanobind (>=2.5.0), * a compiler with C++17 support. To install from source, `Eigen` and `Boost` also need to be available, and CMake will try to find suitable versions automatically. diff --git a/docs/index.rst b/docs/index.rst index 2e2bde23..9845f054 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -15,10 +15,6 @@ pyvinecopulib :target: https://github.com/vinecopulib/pyvinecopulib/actions/workflows/pypi.yml :alt: Build Status -.. image:: https://api.codacy.com/project/badge/Grade/3c0056d3ca5244a5ba6a2b32f87be4cf - :target: https://www.codacy.com/gh/vinecopulib/pyvinecopulib?utm_source=github.com&utm_medium=referral&utm_content=vinecopulib/pyvinecopulib&utm_campaign=Badge_Grade - :alt: Codacy Badge - .. image:: https://zenodo.org/badge/196999069.svg :target: https://zenodo.org/badge/latestdoi/196999069 :alt: DOI diff --git a/lib/vinecopulib b/lib/vinecopulib index b72e5130..51108071 160000 --- a/lib/vinecopulib +++ b/lib/vinecopulib @@ -1 +1 @@ -Subproject commit b72e51308fc48bfaf29a1beb7f4ce79ba44c5917 +Subproject commit 51108071f468a5f115e41eaece6bcf77db7544c9 diff --git a/pyproject.toml b/pyproject.toml index aa2cefec..1af7cb5f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,10 @@ [build-system] -requires = ["scikit-build-core >=0.4.3", "nanobind >=1.3.2"] +requires = ["scikit-build-core >=0.4.3", "nanobind >=2.5.0"] build-backend = "scikit_build_core.build" [project] name = "pyvinecopulib" -version = "0.7.0" +version = "0.7.1" authors = [ { name="Thibault Vatter", email="info@vinecopulib.com" }, { name="Thomas Nagler", email="info@vinecopulib.com" }, diff --git a/scripts/process_changelog.py b/scripts/process_changelog.py index 23a82464..66b593e6 100644 --- a/scripts/process_changelog.py +++ b/scripts/process_changelog.py @@ -36,30 +36,22 @@ def replace_commit(match): # Example changelog changelog_text = """ -#### New features - -* Use analytical derivatives in discrete pdf/hfuncs (#572) -* Allow for alternative for `"prim"` vs `"kruskal"` in MST-based model selection (#577) -* Improve the dependencies install script to use it in other projects (#576) -* Add tawn copula (#579) -* Improve doc (#580, #585, #607) -* Allow for the discrete Rosenblatt transform (#581) -* Add `Vinecop::fit()` (#584) -* Improve `Bicop::str()` (#588) and `Vinecop::str()` (#589) -* Properly handle discrete variables for the TLL family (#597) -* Weighted pseudo-observations (#602) -* Cross-platform random numbers and add seeds options to `to_pseudo_obs` (#603) -* Improve performance by - * aligning with the `R` defaults (e.g., `BOOST_NO_AUTO_PTR`, `BOOST_ALLOW_DEPRECATED_HEADERS`, `BOOST_MATH_PROMOTE_DOUBLE_POLICY=false`, `std::string nonparametric_method = "constant"` for the TLL instead of `"quadratic"`, `-O3 -march=native` compiler flags) and add benchmarking example (#592, #611, #613), - * using `Eigen` element-wise operations instead of `boost` whenever possible (#598, #612), - * using binary search in the TLL for `get_indices` (#613). - -#### Bug fixes - -* Improve stability in BB7 PDF (#573) -* Revamped CI/CD pipeline, tests discoverable by CTest, boost version on windows (66cf8b0) -* Fix ASAN issues (#583) -* Fix interface includes and other CMake issue (#586, #599, #601, #608), by @jschueller +### NEW FEATURES + +* add `allow_rotation` option to `FitControlsBicop` and `FitControlsVinecop` + to allow for the rotation of the pair copulas (#628). + +* add a `FitControlsConfig` struct to create flexible and yet safe constructors + for `FitControlsBicop` and `FitControlsVinecop` (#629). + +### BUG FIXES + +* restrict parameter range for fitting Tawn copulas; fix handling of their + shape/argument order (#620). + +* compute and save loglik/nobs in `Vinecop::fit()` (#623) + +* disable unwanted compiler output related to BOOST_CONCEPT checks (#624) """ # Repository URL diff --git a/src/include/bicop/class.hpp b/src/include/bicop/class.hpp index 9f6c3012..324d0200 100644 --- a/src/include/bicop/class.hpp +++ b/src/include/bicop/class.hpp @@ -3,6 +3,7 @@ #include "docstr.hpp" #include #include +#include #include namespace nb = nanobind; @@ -208,5 +209,23 @@ Alternatives to instantiate bivariate copulas are: nb::cast( nb::module_::import_("pyvinecopulib._python_helpers.bicop") .attr("BICOP_PLOT_DOC")) - .c_str()); + .c_str()) + .def("__getstate__", + [](const Bicop& cop) { + return std::make_tuple(cop.get_family(), + cop.get_rotation(), + cop.get_parameters(), + cop.get_var_types()); + }) + .def("__setstate__", + [](Bicop& cop, + std::tuple> state) { + new (&cop) Bicop(std::get<0>(state), + std::get<1>(state), + std::get<2>(state), + std::get<3>(state)); + }); } \ No newline at end of file diff --git a/src/include/bicop/family.hpp b/src/include/bicop/family.hpp index 2fb00bc6..5ba2909f 100644 --- a/src/include/bicop/family.hpp +++ b/src/include/bicop/family.hpp @@ -46,8 +46,6 @@ init_bicop_family(nb::module_& module) ``bb1``, ``bb7``, ``tawn``), - ``ut`` contains the families that are upper-tail dependent (``gumbel``, ``joe``, ``bb1``, ``bb6``, ``bb7``, ``bb8``, ``tawn``), - - ``flip_by_rotation`` contains families that can be flipped by rotation - (``clayton``, ``gumbel``, ``frank`` , ``joe``, ``bb1``, ``bb6``, ``bb7``, ``bb8``, ``tawn``), - ``rotationless`` contains families that don't have a rotation because they already cover positive and negative dependence (``indep``, ``gaussian``, ``student``, ``frank``, ``tll``). )pbdoc") @@ -80,5 +78,4 @@ init_bicop_family(nb::module_& module) module.attr("lt") = bicop_families::lt; module.attr("ut") = bicop_families::ut; module.attr("itau") = bicop_families::itau; - module.attr("flip_by_rotation") = bicop_families::flip_by_rotation; } \ No newline at end of file diff --git a/src/include/bicop/fit_controls.hpp b/src/include/bicop/fit_controls.hpp index 84e1571f..d28385d4 100644 --- a/src/include/bicop/fit_controls.hpp +++ b/src/include/bicop/fit_controls.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -28,6 +29,7 @@ init_bicop_fit_controls(nb::module_& module) const Eigen::VectorXd&, double, bool, + bool, size_t>(), fitcontrolsbicop_doc.ctor.doc_9args, "family_set"_a = bicop_families::all, @@ -38,6 +40,7 @@ init_bicop_fit_controls(nb::module_& module) "weights"_a = Eigen::VectorXd(), "psi0"_a = 0.9, "preselect_families"_a = true, + "allow_rotations"_a = true, "num_threads"_a = 1) /* .def(nb::init(), */ // "creates default controls except for the parameteric method.", @@ -78,6 +81,10 @@ init_bicop_fit_controls(nb::module_& module) &FitControlsBicop::set_preselect_families, "Whether to exclude families based on symmetry properties " "of the data.") + .def_prop_rw("allow_rotations", + &FitControlsBicop::get_allow_rotations, + &FitControlsBicop::set_allow_rotations, + "Whether to allow rotations for the families.") .def_prop_rw("num_threads", &FitControlsBicop::get_num_threads, &FitControlsBicop::set_num_threads, @@ -86,5 +93,44 @@ init_bicop_fit_controls(nb::module_& module) [](const FitControlsBicop& controls) { return "\n" + controls.str(); }) - .def("str", &FitControlsBicop::str, fitcontrolsbicop_doc.str.doc); + .def("str", &FitControlsBicop::str, fitcontrolsbicop_doc.str.doc) + .def("__getstate__", + [](const FitControlsBicop& controls) { + return std::make_tuple(controls.get_family_set(), + controls.get_parametric_method(), + controls.get_nonparametric_method(), + controls.get_nonparametric_mult(), + controls.get_selection_criterion(), + controls.get_weights(), + controls.get_psi0(), + controls.get_preselect_families(), + controls.get_allow_rotations(), + controls.get_num_threads()); + }) + .def("__setstate__", + [](FitControlsBicop& controls, + std::tuple, + std::string, + std::string, + double, + std::string, + const Eigen::VectorXd&, + double, + bool, + bool, + size_t> state) { + FitControlsConfig config; + config.family_set = std::get<0>(state); + config.parametric_method = std::get<1>(state); + config.nonparametric_method = std::get<2>(state); + config.nonparametric_mult = std::get<3>(state); + config.selection_criterion = std::get<4>(state); + config.weights = std::get<5>(state); + config.psi0 = std::get<6>(state); + config.preselect_families = std::get<7>(state); + config.allow_rotations = std::get<8>(state); + config.num_threads = std::get<9>(state); + + new (&controls) FitControlsBicop(config); + }); } \ No newline at end of file diff --git a/src/include/vinecop/class.hpp b/src/include/vinecop/class.hpp index f817a3aa..5af9e060 100644 --- a/src/include/vinecop/class.hpp +++ b/src/include/vinecop/class.hpp @@ -330,5 +330,21 @@ empty vine copula of a given dimension. It can then be used to select a model fr nb::cast( nb::module_::import_("pyvinecopulib._python_helpers.vinecop") .attr("VINECOP_PLOT_DOC")) - .c_str()); + .c_str()) + .def("__getstate__", + [](const Vinecop& cop) { + return std::make_tuple(cop.get_rvine_structure().to_json().dump(), + cop.get_all_pair_copulas(), + cop.get_var_types()); + }) + .def("__setstate__", + [](Vinecop& cop, + std::tuple>, + std::vector> state) { + nlohmann::json json_obj = nlohmann::json::parse(std::get<0>(state)); + RVineStructure structure = RVineStructure(json_obj); + new (&cop) + Vinecop(structure, std::get<1>(state), std::get<2>(state)); + }); } \ No newline at end of file diff --git a/src/include/vinecop/fit_controls.hpp b/src/include/vinecop/fit_controls.hpp index 556994b2..62eedbda 100644 --- a/src/include/vinecop/fit_controls.hpp +++ b/src/include/vinecop/fit_controls.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -36,7 +37,8 @@ init_vinecop_fit_controls(nb::module_& module) bool, bool, size_t, - std::string>(), + std::string, + bool>(), "family_set"_a = bicop_families::all, "parametric_method"_a = "mle", "nonparametric_method"_a = "constant", @@ -54,6 +56,7 @@ init_vinecop_fit_controls(nb::module_& module) "show_trace"_a = false, "num_threads"_a = 1, "mst_algorithm"_a = "prim", + "allow_rotations"_a = true, fitcontrolsvinecop_doc.ctor.doc_17args) .def_prop_rw("family_set", &FitControlsVinecop::get_family_set, @@ -120,9 +123,80 @@ init_vinecop_fit_controls(nb::module_& module) &FitControlsVinecop::get_num_threads, &FitControlsVinecop::set_num_threads, "The number of threads.") + .def_prop_rw("mst_algorithm", + &FitControlsVinecop::get_mst_algorithm, + &FitControlsVinecop::set_mst_algorithm, + "The minimum spanning tree algorithm.") + .def_prop_rw("allow_rotations", + &FitControlsVinecop::get_allow_rotations, + &FitControlsVinecop::set_allow_rotations, + "Whether to allow rotations for the families.") .def("__repr__", [](const FitControlsVinecop& controls) { return "\n" + controls.str(); }) - .def("str", &FitControlsVinecop::str, fitcontrolsvinecop_doc.str.doc); + .def("str", &FitControlsVinecop::str, fitcontrolsvinecop_doc.str.doc) + .def("__getstate__", + [](const FitControlsVinecop& controls) { + return std::make_tuple(controls.get_family_set(), + controls.get_parametric_method(), + controls.get_nonparametric_method(), + controls.get_nonparametric_mult(), + controls.get_trunc_lvl(), + controls.get_tree_criterion(), + controls.get_threshold(), + controls.get_selection_criterion(), + controls.get_weights(), + controls.get_psi0(), + controls.get_preselect_families(), + controls.get_select_trunc_lvl(), + controls.get_select_threshold(), + controls.get_select_families(), + controls.get_show_trace(), + controls.get_num_threads(), + controls.get_mst_algorithm(), + controls.get_allow_rotations()); + }) + .def("__setstate__", + [](FitControlsVinecop& controls, + std::tuple, + std::string, + std::string, + double, + size_t, + std::string, + double, + std::string, + Eigen::VectorXd, + double, + bool, + bool, + bool, + bool, + bool, + size_t, + std::string, + bool> state) { + FitControlsConfig config; + config.family_set = std::get<0>(state); + config.parametric_method = std::get<1>(state); + config.nonparametric_method = std::get<2>(state); + config.nonparametric_mult = std::get<3>(state); + config.trunc_lvl = std::get<4>(state); + config.tree_criterion = std::get<5>(state); + config.threshold = std::get<6>(state); + config.selection_criterion = std::get<7>(state); + config.weights = std::get<8>(state); + config.psi0 = std::get<9>(state); + config.preselect_families = std::get<10>(state); + config.select_trunc_lvl = std::get<11>(state); + config.select_threshold = std::get<12>(state); + config.select_families = std::get<13>(state); + config.show_trace = std::get<14>(state); + config.num_threads = std::get<15>(state); + config.mst_algorithm = std::get<16>(state); + config.allow_rotations = std::get<17>(state); + + new (&controls) FitControlsVinecop(config); + }); } \ No newline at end of file diff --git a/src/include/vinecop/rvine_structure.hpp b/src/include/vinecop/rvine_structure.hpp index 4c47c7c0..f6134c4e 100644 --- a/src/include/vinecop/rvine_structure.hpp +++ b/src/include/vinecop/rvine_structure.hpp @@ -48,7 +48,8 @@ rv_from_file(const std::string& filename, bool check = true) inline RVineStructure rv_from_json(const std::string& json, bool check = true) { - return RVineStructure(json, check); + nlohmann::json json_obj = nlohmann::json::parse(json); + return RVineStructure(json_obj, check); } inline void @@ -158,7 +159,15 @@ Alternatives to instantiate structures are: [](const RVineStructure& rvs) { return "\n" + rvs.str(); }) - .def("str", &RVineStructure::str, rvinestructure_doc.str.doc); + .def("str", &RVineStructure::str, rvinestructure_doc.str.doc) + .def("__getstate__", + [](const RVineStructure& rvinestruct) { + return rvinestruct.to_json().dump(); + }) + .def("__setstate__", [](RVineStructure& rvinestruct, std::string state) { + nlohmann::json json_obj = nlohmann::json::parse(state); + new (&rvinestruct) RVineStructure(json_obj); + }); nb::class_( module, "DVineStructure", dvinestructure_doc.doc) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/helpers.py b/tests/helpers.py new file mode 100644 index 00000000..fe2d804d --- /dev/null +++ b/tests/helpers.py @@ -0,0 +1,52 @@ +import numpy as np + + +def random_data(d=5, n=1000): + # Simulate some data + np.random.seed(1234) # seed for the random generator + mean = np.random.normal(size=d) # mean vector + cov = np.random.normal(size=(d, d)) # covariance matrix + cov = np.dot(cov.transpose(), cov) # make it non-negative definite + x = np.random.multivariate_normal(mean, cov, n) + return x + + +def compare_properties(obj1, obj2, attrs, subclass=False): + if subclass: + assert issubclass( + type(obj1), type(obj2) + ), "Objects must be of the same type" + else: + assert type(obj1) is type(obj2), "Objects must be of the same type" + for attr in attrs: + val1 = getattr(obj1, attr) + val2 = getattr(obj2, attr) + if isinstance(val1, np.ndarray): + assert isinstance(val2, np.ndarray) and np.array_equal( + val1, val2 + ), f"Mismatch in {attr}: {val1} != {val2}" + else: + assert val1 == val2, f"Mismatch in {attr}: {val1} != {val2}" + + +def compare_bicop(cop1, cop2): + attrs = ["family", "rotation", "parameters", "var_types"] + compare_properties(cop1, cop2, attrs) + + +def compare_vinecop(cop1, cop2): + attrs = ["dim", "trunc_lvl", "order", "matrix"] + compare_properties(cop1, cop2, attrs) + + d = cop1.dim + trunc_lvl = cop1.trunc_lvl + for t in range(trunc_lvl): + for e in range(d - t - 1): + bicop1 = cop1.get_pair_copula(t, e) + bicop2 = cop2.get_pair_copula(t, e) + compare_bicop(bicop1, bicop2) + + +def compare_rvinestructure(struct1, struct2, subclass=False): + attrs = ["dim", "order", "trunc_lvl", "matrix"] + compare_properties(struct1, struct2, attrs, subclass) diff --git a/tests/test_bicop.py b/tests/test_bicop.py index 062e78f8..522f4dea 100644 --- a/tests/test_bicop.py +++ b/tests/test_bicop.py @@ -62,8 +62,13 @@ def test_bicop(): with pytest.raises(AttributeError): bicop.npars = 2 - # Test loglik method + # Test passing a single row of data (#169 & #170 fix) bicop.var_types = ["c", "c"] + u = np.array([[0.1, 0.2]]) + d = bicop.pdf(u) + assert isinstance(d, np.ndarray) and d.shape == (1,) + + # Test loglik method u = np.array([[0.1, 0.2], [0.3, 0.4]]) loglik = bicop.loglik(u) assert isinstance(loglik, float) diff --git a/tests/test_pickling.py b/tests/test_pickling.py new file mode 100644 index 00000000..88f8453a --- /dev/null +++ b/tests/test_pickling.py @@ -0,0 +1,121 @@ +import pickle + +import numpy as np + +import pyvinecopulib as pv + +from .helpers import ( + compare_bicop, + compare_properties, + compare_rvinestructure, + compare_vinecop, + random_data, +) + + +def test_fitcontrolsbicop(): + original_controls = pv.FitControlsBicop() + original_controls.family_set = pv.itau + original_controls.parametric_method = "itau" + + # Serialize the object + serialized = pickle.dumps(original_controls) + + # Deserialize the object + deserialized_controls = pickle.loads(serialized) + + # Ensure the deserialized object has the same attributes as the original + attrs = [ + "family_set", + "parametric_method", + "nonparametric_method", + "nonparametric_mult", + "selection_criterion", + "weights", + "psi0", + "preselect_families", + "num_threads", + ] + compare_properties(original_controls, deserialized_controls, attrs) + + +def test_fitcontrolsvinecop(): + # Create an instance of FitControlsVinecop with some configuration + original_controls = pv.FitControlsVinecop() + original_controls.family_set = pv.itau + original_controls.parametric_method = "itau" + + # Serialize the object + serialized = pickle.dumps(original_controls) + + # Deserialize the object + deserialized_controls = pickle.loads(serialized) + + # Ensure the deserialized object has the same attributes as the original + attrs = [ + "family_set", + "parametric_method", + "nonparametric_method", + "weights", + "nonparametric_mult", + "trunc_lvl", + "tree_criterion", + "threshold", + "selection_criterion", + "psi0", + "preselect_families", + "select_trunc_lvl", + "select_threshold", + "select_families", + "show_trace", + "num_threads", + "mst_algorithm", + ] + compare_properties(original_controls, deserialized_controls, attrs) + + +def test_bicop(): + original_bicop = pv.Bicop(pv.BicopFamily.gaussian) + original_bicop.parameters = np.array([[0.5]]) + + # Serialize the object + serialized = pickle.dumps(original_bicop) + + # Deserialize the object + deserialized_bicop = pickle.loads(serialized) + + # Assert that the deserialized object's properties match the original + compare_bicop(original_bicop, deserialized_bicop) + + +def test_rvinestructure(): + # Create an instance of RVineStructure with some configuration + original_structure = pv.RVineStructure.simulate(5) + + # Serialize the object + serialized = pickle.dumps(original_structure) + + # Deserialize the object + deserialized_structure = pickle.loads(serialized) + + # Ensure the deserialized object has the same attributes as the original + compare_rvinestructure(original_structure, deserialized_structure) + + +def test_vinecop(): + d = 5 + n = 1000 + u = pv.to_pseudo_obs(random_data(d, n)) + + controls = pv.FitControlsVinecop(family_set=[pv.BicopFamily.gaussian]) + assert controls.family_set == [pv.BicopFamily.gaussian] + original_cop = pv.Vinecop.from_data(u, controls=controls) + + # Serialize the object + serialized = pickle.dumps(original_cop) + + # Deserialize the object + deserialized_cop = pickle.loads(serialized) + + # Ensure the deserialized object has the same attributes as the original + compare_vinecop(original_cop, deserialized_cop) diff --git a/tests/test_vinecop.py b/tests/test_vinecop.py index f654c6fd..0b3c54e4 100644 --- a/tests/test_vinecop.py +++ b/tests/test_vinecop.py @@ -5,15 +5,8 @@ import pyvinecopulib as pv +from .helpers import compare_rvinestructure, compare_vinecop, random_data -def random_data(d=5, n=1000): - # Simulate some data - np.random.seed(1234) # seed for the random generator - mean = np.random.normal(size=d) # mean vector - cov = np.random.normal(size=(d, d)) # covariance matrix - cov = np.dot(cov.transpose(), cov) # make it non-negative definite - x = np.random.multivariate_normal(mean, cov, n) - return x def test_vinecop(): d = 5 @@ -49,6 +42,13 @@ def test_vinecop(): assert isinstance(values, np.ndarray) assert values.shape == (n,) + # Test passing a single row of data (#169 & #170 fix) + u1 = u[0, :].reshape(1, d) + for method in ["pdf", "cdf"]: + values = getattr(cop, method)(u1) + assert isinstance(values, np.ndarray) + assert values.shape == (1,) + # Test simulate method simulated_data = cop.simulate(n) assert simulated_data.shape == (n, d) @@ -79,17 +79,40 @@ def test_vinecop(): # Test to_json and from_json new_cop = pv.Vinecop.from_json(cop.to_json()) - assert new_cop.dim == cop.dim - assert new_cop.trunc_lvl == cop.trunc_lvl - assert new_cop.order == cop.order + compare_vinecop(cop, new_cop) test_folder = "test_dump" os.makedirs(test_folder, exist_ok=True) filename = test_folder + "/test_vinecop.json" cop.to_file(filename) new_cop = pv.Vinecop.from_file(filename) - assert new_cop.dim == cop.dim - assert new_cop.trunc_lvl == cop.trunc_lvl - assert new_cop.order == cop.order + compare_vinecop(cop, new_cop) + + # Clean up + shutil.rmtree(test_folder) + + +def test_rvinestructure(): + # Test RVineStructure class + rvine = pv.RVineStructure(5) + assert isinstance(rvine, pv.RVineStructure) + assert rvine.dim == 5 + assert rvine.matrix.shape == (5, 5) + assert rvine.trunc_lvl == 4 + assert rvine.order == list(range(1, 6)) + + # Should be the same as the previous one + dvine = pv.DVineStructure(rvine.order) + compare_rvinestructure(dvine, rvine, True) + + # Test to_json and from_json + new_rvine = pv.RVineStructure.from_json(rvine.to_json()) + compare_rvinestructure(rvine, new_rvine) + test_folder = "test_dump" + os.makedirs(test_folder, exist_ok=True) + filename = test_folder + "/test_rvine.json" + rvine.to_file(filename) + new_rvine = pv.RVineStructure.from_file(filename) + compare_rvinestructure(rvine, new_rvine) # Clean up shutil.rmtree(test_folder)