/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ #include "kll_sketch.hpp" #include #include #include #include #include #include namespace py = pybind11; namespace datasketches { namespace vector_of_kll_constants { static const uint32_t DEFAULT_K = kll_constants::DEFAULT_K; static const uint32_t DEFAULT_D = 1; } // Wrapper class for Numpy compatibility template > class vector_of_kll_sketches { public: explicit vector_of_kll_sketches(uint32_t k = vector_of_kll_constants::DEFAULT_K, uint32_t d = vector_of_kll_constants::DEFAULT_D); vector_of_kll_sketches(const vector_of_kll_sketches& other); vector_of_kll_sketches(vector_of_kll_sketches&& other) noexcept; vector_of_kll_sketches& operator=(const vector_of_kll_sketches& other); vector_of_kll_sketches& operator=(vector_of_kll_sketches&& other); // container parameters inline uint32_t get_k() const; inline uint32_t get_d() const; // sketch updates/merges void update(const py::array_t& items); void merge(const vector_of_kll_sketches& other); // returns a single sketch combining all data in the array kll_sketch collapse(const py::array_t& isk) const; // sketch queries returning an array of results py::array is_empty() const; py::array get_n() const; py::array is_estimation_mode() const; py::array get_min_values() const; py::array get_max_values() const; py::array get_num_retained() const; py::array get_quantiles(const py::array_t& ranks, const py::array_t& isk) const; py::array get_ranks(const py::array_t& values, const py::array_t& isk) const; py::array get_pmf(const py::array_t& split_points, const py::array_t& isk) const; py::array get_cdf(const py::array_t& split_points, const py::array_t& isk) const; // human-readable output std::string to_string(bool print_levels = false, bool print_items = false) const; // binary output/input py::list serialize(py::array_t& isk); // note: deserialize() replaces the sketch at the specified // index. Not a static method. void deserialize(const py::bytes& sk_bytes, uint32_t idx); private: std::vector get_indices(const py::array_t& isk) const; const uint32_t k_; // kll sketch k parameter const uint32_t d_; // number of dimensions (here: sketches) to hold std::vector> sketches_; }; template vector_of_kll_sketches::vector_of_kll_sketches(uint32_t k, uint32_t d): k_(k), d_(d) { // check d is valid (k is checked by kll_sketch) if (d < 1) { throw std::invalid_argument("D must be >= 1: " + std::to_string(d)); } sketches_.reserve(d); // spawn the sketches for (uint32_t i = 0; i < d; i++) { sketches_.emplace_back(k); } } template vector_of_kll_sketches::vector_of_kll_sketches(const vector_of_kll_sketches& other) : k_(other.k_), d_(other.d_), sketches_(other.sketches_) {} template vector_of_kll_sketches::vector_of_kll_sketches(vector_of_kll_sketches&& other) noexcept : k_(other.k_), d_(other.d_), sketches_(std::move(other.sketches_)) {} template vector_of_kll_sketches& vector_of_kll_sketches::operator=(const vector_of_kll_sketches& other) { vector_of_kll_sketches copy(other); k_ = copy.k_; d_ = copy.d_; std::swap(sketches_, copy.sketches_); return *this; } template vector_of_kll_sketches& vector_of_kll_sketches::operator=(vector_of_kll_sketches&& other) { k_ = other.k_; d_ = other.d_; std::swap(sketches_, other.sketches_); return *this; } template uint32_t vector_of_kll_sketches::get_k() const { return k_; } template uint32_t vector_of_kll_sketches::get_d() const { return d_; } template std::vector vector_of_kll_sketches::get_indices(const py::array_t& isk) const { std::vector indices; if (isk.size() == 1) { auto data = isk.unchecked(); if (data(0) == -1) { indices.reserve(d_); for (uint32_t i = 0; i < d_; ++i) { indices.push_back(i); } } else { indices.push_back(static_cast(data(0))); } } else { auto data = isk.unchecked<1>(); indices.reserve(isk.size()); for (uint32_t i = 0; i < isk.size(); ++i) { const uint32_t idx = static_cast(data(i)); if (idx < d_) { indices.push_back(idx); } else { throw std::invalid_argument("request for invalid dimenions >= d (" + std::to_string(d_) +"): "+ std::to_string(idx)); } } } return indices; } // Checks if each sketch is empty or not template py::array vector_of_kll_sketches::is_empty() const { std::vector vals(d_); for (uint32_t i = 0; i < d_; ++i) { vals[i] = sketches_[i].is_empty(); } return py::cast(vals); } // Updates each sketch with values // Currently: all values must be present // TODO: allow subsets of sketches to be updated template void vector_of_kll_sketches::update(const py::array_t& items) { size_t ndim = items.ndim(); if (items.shape(ndim-1) != d_) { throw std::invalid_argument("input data must have rows with " + std::to_string(d_) + " elements. Found: " + std::to_string(items.shape(ndim-1))); } if (ndim == 1) { // 1D case: single value to update per sketch auto data = items.template unchecked<1>(); for (uint32_t i = 0; i < d_; ++i) { sketches_[i].update(data(i)); } } else if (ndim == 2) { // 2D case: multiple values to update per sketch auto data = items.template unchecked<2>(); if (items.flags() & py::array::f_style) { for (uint32_t j = 0; j < d_; ++j) { for (uint32_t i = 0; i < items.shape(0); ++i) { sketches_[j].update(data(i,j)); } } } else { // py::array::c_style or py::array::forcecast for (uint32_t i = 0; i < items.shape(0); ++i) { for (uint32_t j = 0; j < d_; ++j) { sketches_[j].update(data(i,j)); } } } } else { throw std::invalid_argument("Update input must be 2 or fewer dimensions : " + std::to_string(ndim)); } } // Merges two arrays of sketches // Currently: all values must be present template void vector_of_kll_sketches::merge(const vector_of_kll_sketches& other) { if (d_ != other.get_d()) { throw std::invalid_argument("Must have same number of dimensions to merge: " + std::to_string(d_) + " vs " + std::to_string(other.d_)); } else { for (uint32_t i = 0; i < d_; ++i) { sketches_[i].merge(other.sketches_[i]); } } } template kll_sketch vector_of_kll_sketches::collapse(const py::array_t& isk) const { std::vector inds = get_indices(isk); kll_sketch result(k_); for (auto& idx : inds) { result.merge(sketches_[idx]); } return result; } // Number of updates for each sketch template py::array vector_of_kll_sketches::get_n() const { std::vector vals(d_); for (uint32_t i = 0; i < d_; ++i) { vals[i] = sketches_[i].get_n(); } return py::cast(vals); } // Number of retained values for each sketch template py::array vector_of_kll_sketches::get_num_retained() const { std::vector vals(d_); for (uint32_t i = 0; i < d_; ++i) { vals[i] = sketches_[i].get_num_retained(); } return py::cast(vals); } // Gets the minimum value of each sketch // TODO: allow subsets of sketches template py::array vector_of_kll_sketches::get_min_values() const { std::vector vals(d_); for (uint32_t i = 0; i < d_; ++i) { vals[i] = sketches_[i].get_min_item(); } return py::cast(vals); } // Gets the maximum value of each sketch // TODO: allow subsets of sketches template py::array vector_of_kll_sketches::get_max_values() const { std::vector vals(d_); for (uint32_t i = 0; i < d_; ++i) { vals[i] = sketches_[i].get_max_item(); } return py::cast(vals); } // Summary of each sketch as one long string // Users should use .split('\n\n') when calling it to build a list of each // sketch's summary template std::string vector_of_kll_sketches::to_string(bool print_levels, bool print_items) const { std::ostringstream ss; for (uint32_t i = 0; i < d_; ++i) { // all streams into 1 string, for compatibility with Python's str() behavior // users will need to split by \n\n, e.g., str(kll).split('\n\n') if (i > 0) ss << "\n"; ss << sketches_[i].to_string(print_levels, print_items); } return ss.str(); } template py::array vector_of_kll_sketches::is_estimation_mode() const { std::vector vals(d_); for (uint32_t i = 0; i < d_; ++i) { vals[i] = sketches_[i].is_estimation_mode(); } return py::cast(vals); } // Value of sketch(es) corresponding to some quantile(s) template py::array vector_of_kll_sketches::get_quantiles(const py::array_t& ranks, const py::array_t& isk) const { std::vector inds = get_indices(isk); size_t num_sketches = inds.size(); size_t num_quantiles = ranks.size(); std::vector> quants(num_sketches, std::vector(num_quantiles)); for (uint32_t i = 0; i < num_sketches; ++i) { for (size_t j = 0; j < num_quantiles; ++j) { quants[i][j] = sketches_[inds[i]].get_quantile(ranks.data()[j]); } } return py::cast(quants); } // Value of sketch(es) corresponding to some rank(s) template py::array vector_of_kll_sketches::get_ranks(const py::array_t& values, const py::array_t& isk) const { std::vector inds = get_indices(isk); size_t num_sketches = inds.size(); size_t num_ranks = values.size(); auto vals = values.data(); std::vector> ranks(num_sketches, std::vector(num_ranks)); for (uint32_t i = 0; i < num_sketches; ++i) { for (size_t j = 0; j < num_ranks; ++j) { ranks[i][j] = sketches_[inds[i]].get_rank(vals[j]); } } return py::cast(ranks); } // PMF(s) of sketch(es) template py::array vector_of_kll_sketches::get_pmf(const py::array_t& split_points, const py::array_t& isk) const { std::vector inds = get_indices(isk); size_t num_sketches = inds.size(); size_t num_splits = split_points.size(); std::vector> pmfs(num_sketches, std::vector(num_splits + 1)); for (uint32_t i = 0; i < num_sketches; ++i) { auto pmf = sketches_[inds[i]].get_PMF(split_points.data(), num_splits); for (size_t j = 0; j <= num_splits; ++j) { pmfs[i][j] = pmf[j]; } } return py::cast(pmfs); } // CDF(s) of sketch(es) template py::array vector_of_kll_sketches::get_cdf(const py::array_t& split_points, const py::array_t& isk) const { std::vector inds = get_indices(isk); size_t num_sketches = inds.size(); size_t num_splits = split_points.size(); std::vector> cdfs(num_sketches, std::vector(num_splits + 1)); for (uint32_t i = 0; i < num_sketches; ++i) { auto cdf = sketches_[inds[i]].get_CDF(split_points.data(), num_splits); for (size_t j = 0; j <= num_splits; ++j) { cdfs[i][j] = cdf[j]; } } return py::cast(cdfs); } template void vector_of_kll_sketches::deserialize(const py::bytes& sk_bytes, uint32_t idx) { if (idx >= d_) { throw std::invalid_argument("request for invalid dimenions >= d (" + std::to_string(d_) +"): "+ std::to_string(idx)); } std::string skStr = sk_bytes; // implicit cast // load the sketch into the proper index sketches_[idx] = std::move(kll_sketch::deserialize(skStr.c_str(), skStr.length())); } template py::list vector_of_kll_sketches::serialize(py::array_t& isk) { std::vector inds = get_indices(isk); const size_t num_sketches = inds.size(); py::list list(num_sketches); for (uint32_t i = 0; i < num_sketches; ++i) { auto serResult = sketches_[inds[i]].serialize(); list[i] = py::bytes((char*)serResult.data(), serResult.size()); } return list; } namespace python { template double kll_sketch_generic_normalized_rank_error(uint16_t k, bool pmf) { return kll_sketch::get_normalized_rank_error(k, pmf); } } // namespace datasketches::python } // namespace datasketches namespace dspy = datasketches::python; template void bind_vector_of_kll_sketches(py::module &m, const char* name) { using namespace datasketches; py::class_>(m, name) .def(py::init(), py::arg("k")=vector_of_kll_constants::DEFAULT_K, py::arg("d")=vector_of_kll_constants::DEFAULT_D) .def(py::init&>()) // allow user to retrieve k or d, in case it's instantiated w/ defaults .def("get_k", &vector_of_kll_sketches::get_k, "Returns the value of `k` of the sketch(es)") .def("get_d", &vector_of_kll_sketches::get_d, "Returns the number of sketches") .def("update", &vector_of_kll_sketches::update, py::arg("items"), "Updates the sketch(es) with value(s). Must be a 1D array of size equal to the number of sketches. Can also be 2D array of shape (n_updates, n_sketches). If a sketch does not have a value to update, use np.nan") .def("__str__", &vector_of_kll_sketches::to_string, py::arg("print_levels")=false, py::arg("print_items")=false, "Produces a string summary of all sketches. Users should split the returned string by '\n\n'") .def("to_string", &vector_of_kll_sketches::to_string, py::arg("print_levels")=false, py::arg("print_items")=false, "Produces a string summary of all sketches. Users should split the returned string by '\n\n'") .def("is_empty", &vector_of_kll_sketches::is_empty, "Returns whether the sketch(es) is(are) empty of not") .def("get_n", &vector_of_kll_sketches::get_n, "Returns the number of values seen by the sketch(es)") .def("get_num_retained", &vector_of_kll_sketches::get_num_retained, "Returns the number of values retained by the sketch(es)") .def("is_estimation_mode", &vector_of_kll_sketches::is_estimation_mode, "Returns whether the sketch(es) is(are) in estimation mode") .def("get_min_values", &vector_of_kll_sketches::get_min_values, "Returns the minimum value(s) of the sketch(es)") .def("get_max_values", &vector_of_kll_sketches::get_max_values, "Returns the maximum value(s) of the sketch(es)") .def("get_quantiles", &vector_of_kll_sketches::get_quantiles, py::arg("ranks"), py::arg("isk")=-1, "Returns the value(s) associated with the specified quantile(s) for the specified sketch(es). `ranks` can be a float between 0 and 1 (inclusive), or a list/array of values. `isk` specifies which sketch(es) to return the value(s) for (default: all sketches)") .def("get_ranks", &vector_of_kll_sketches::get_ranks, py::arg("values"), py::arg("isk")=-1, "Returns the value(s) associated with the specified ranks(s) for the specified sketch(es). `values` can be an int between 0 and the number of values retained, or a list/array of values. `isk` specifies which sketch(es) to return the value(s) for (default: all sketches)") .def("get_pmf", &vector_of_kll_sketches::get_pmf, py::arg("split_points"), py::arg("isk")=-1, "Returns the probability mass function (PMF) at `split_points` of the specified sketch(es). `split_points` should be a list/array of floats between 0 and 1 (inclusive). `isk` specifies which sketch(es) to return the PMF for (default: all sketches)") .def("get_cdf", &vector_of_kll_sketches::get_cdf, py::arg("split_points"), py::arg("isk")=-1, "Returns the cumulative distribution function (CDF) at `split_points` of the specified sketch(es). `split_points` should be a list/array of floats between 0 and 1 (inclusive). `isk` specifies which sketch(es) to return the CDF for (default: all sketches)") .def_static("get_normalized_rank_error", &dspy::kll_sketch_generic_normalized_rank_error, py::arg("k"), py::arg("as_pmf"), "Returns the normalized rank error") .def("serialize", &vector_of_kll_sketches::serialize, py::arg("isk")=-1, "Serializes the specified sketch(es). `isk` can be an int or a list/array of ints (default: all sketches)") .def("deserialize", &vector_of_kll_sketches::deserialize, py::arg("skBytes"), py::arg("isk"), "Deserializes the specified sketch. `isk` must be an int.") .def("merge", &vector_of_kll_sketches::merge, py::arg("array_of_sketches"), "Merges the input array of KLL sketches into the existing array.") .def("collapse", &vector_of_kll_sketches::collapse, py::arg("isk")=-1, "Returns the result of collapsing all sketches in the array into a single sketch. 'isk' can be an int or a list/array of ints (default: all sketches)") ; } void init_vector_of_kll(py::module &m) { bind_vector_of_kll_sketches(m, "vector_of_kll_ints_sketches"); bind_vector_of_kll_sketches(m, "vector_of_kll_floats_sketches"); }