Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

simple_continued_fraction: added public functions to access and modify the coefficients #971

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 40 additions & 2 deletions doc/internals/simple_continued_fraction.qbk
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[/
Copyright Nick Thompson, 2020
Copyright Matt Borland, 2023
Distributed under the Boost Software License, Version 1.0.
(See accompanying file LICENSE_1_0.txt or copy at
http://www.boost.org/LICENSE_1_0.txt).
Expand All @@ -19,9 +20,17 @@

Real khinchin_harmonic_mean() const;

template<typename T, typename Z_>
friend std::ostream& operator<<(std::ostream& out, simple_continued_fraction<T, Z>& scf);
const std::vector<Z>& partial_denominators() const;

inline std::vector<Z>&& get_data() noexcept;

template<typename T, typename Z2>
friend std::ostream& operator<<(std::ostream& out, simple_continued_fraction<T, Z2>& scf);
};

template<typename Real, typename Z = int64_t>
inline std::vector<Z> simple_continued_fraction_coefficients(Real x);

}


Expand All @@ -47,6 +56,35 @@ This is because when examining known values like π, it creates a large number o
It may be the case the a few incorrect partial convergents is harmless, but we compute continued fractions because we would like to do something with them.
One sensible thing to do it to ask whether the number is in some sense "random"; a question that can be partially answered by computing the Khinchin geometric mean

If you only require the coefficients of the simple continued fraction for example in the calculation of [@https://en.wikipedia.org/wiki/Continued_fraction#Best_rational_approximations best rational approximations] there is a free function for that.

An example of this calculation follows:

using boost::math::tools::simple_continued_fraction_coefficients;

auto coefs1 = simple_continued_fraction_coefficients(static_cast<Real>(3.14155L)); // [3; 7, 15, 2, 7, 1, 4, 2]
auto coefs2 = simple_continued_fraction_coefficients(static_cast<Real>(3.14165L)); // [3; 7, 16, 1, 3, 4, 2, 4]

const std::size_t max_size = (std::min)(coefs1.size(), coefs2.size());
std::vector<std::int64_t> coefs;
coefs.reserve(max_size);

for (std::size_t i = 0; i < max_size; ++i)
{
const auto c1 = coefs1[i];
const auto c2 = coefs2[i];
if (c1 == c2)
{
coefs.emplace_back(c1);
continue;
}

coefs.emplace_back((std::min)(c1, c2) + 1);
break;
}

// Result is [3; 7, 16]

[$../equations/khinchin_geometric.svg]

and Khinchin harmonic mean
Expand Down
146 changes: 109 additions & 37 deletions include/boost/math/tools/simple_continued_fraction.hpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// (C) Copyright Nick Thompson 2020.
// (C) Copyright Matt Borland 2023.
// Use, modification and distribution are subject to the
// Boost Software License, Version 1.0. (See accompanying file
// LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
Expand All @@ -14,12 +15,15 @@
#include <limits>
#include <stdexcept>
#include <sstream>
#include <utility>
#include <cstdint>
#include <cassert>

#include <boost/math/tools/is_standalone.hpp>
#ifndef BOOST_MATH_STANDALONE
#include <boost/config.hpp>
#ifdef BOOST_NO_CXX17_IF_CONSTEXPR
#error "The header <boost/math/norms.hpp> can only be used in C++17 and later."
#error "The header <boost/math/simple_continued_fraction.hpp> can only be used in C++17 and later."
#endif
#endif

Expand All @@ -32,34 +36,59 @@ namespace boost::math::tools {
template<typename Real, typename Z = int64_t>
class simple_continued_fraction {
public:
simple_continued_fraction(Real x) : x_{x} {
typedef Z int_type;

simple_continued_fraction(std::vector<Z> data) : b_{std::move(data)} {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
simple_continued_fraction(std::vector<Z> data) : b_{std::move(data)} {
simple_continued_fraction(std::vector<Z>&& data) : b_{data} {

As written this makes a copy of the data vector and then moves it into b_. I expect you want to take an r-value reference instead.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

simple_continued_fraction(std::vector<Z>) can be used both with std::vector<Z>&& and const std::vector<Z>& arguments, whereas simple_continued_fraction(std::vector<Z>&&) cannot be used with const std::vector<Z>&. In the case of std::vector<Z>&& argument, there should no significant difference (std::vector<Z> can require one more call to move constructor, which is negligible).

More importantly, I'm pretty sure that your suggestion simple_continued_fraction(std::vector<Z>&& data) : b_{data} will call copy constructor on data, because you pass it as an lvalue, not as an rvalue.

const size_t size_ = b_.size();
if (size_ == 0) {
throw std::length_error("Array of coefficients is empty.");
}

for (size_t i = 1; i < size_; ++i) {
if (b_[i] <= 0) {
std::ostringstream oss;
oss << "Found a negative partial denominator: b[" << i << "] = " << b_[i] << ".";
throw std::domain_error(oss.str());
}
}

canonicalize();
}

simple_continued_fraction(Real x) : b_{} {
using std::floor;
using std::abs;
using std::sqrt;
using std::isfinite;
if (!isfinite(x)) {
throw std::domain_error("Cannot convert non-finites into continued fractions.");
throw std::domain_error("Cannot convert non-finites into continued fractions.");
}

if constexpr (std_precision == 2147483647) {
precision_ = x.backend().precision();
}

b_.reserve(50);
Real bj = floor(x);
b_.push_back(static_cast<Z>(bj));
if (bj == x) {
b_.shrink_to_fit();
return;
}

const Real orig_x = x;
x = 1/(x-bj);
Real f = bj;
if (bj == 0) {
f = 16*(std::numeric_limits<Real>::min)();
}
Real C = f;
Real D = 0;
int i = 0;
// the "1 + i++" lets the error bound grow slowly with the number of convergents.
// the "1 + i" lets the error bound grow slowly with the number of convergents.
// I have not worked out the error propagation of the Modified Lentz's method to see if it does indeed grow at this rate.
// Numerical Recipes claims that no one has worked out the error analysis of the modified Lentz's method.
while (abs(f - x_) >= (1 + i++)*std::numeric_limits<Real>::epsilon()*abs(x_))
{
const Real eps_abs_orig_x = std::numeric_limits<Real>::epsilon()*abs(orig_x);
for (int i = 0; abs(f - orig_x) >= (1 + i)*eps_abs_orig_x; ++i) {
bj = floor(x);
b_.push_back(static_cast<Z>(bj));
x = 1/(x-bj);
Expand All @@ -74,16 +103,10 @@ class simple_continued_fraction {
D = 1/D;
f *= (C*D);
}
// Deal with non-uniqueness of continued fractions: [a0; a1, ..., an, 1] = a0; a1, ..., an + 1].
// The shorter representation is considered the canonical representation,
// so if we compute a non-canonical representation, change it to canonical:
if (b_.size() > 2 && b_.back() == 1) {
b_[b_.size() - 2] += 1;
b_.resize(b_.size() - 1);
}
b_.shrink_to_fit();

for (size_t i = 1; i < b_.size(); ++i) {
canonicalize();

const size_t size_ = b_.size();
for (size_t i = 1; i < size_; ++i) {
if (b_[i] <= 0) {
std::ostringstream oss;
oss << "Found a negative partial denominator: b[" << i << "] = " << b_[i] << "."
Expand All @@ -98,19 +121,20 @@ class simple_continued_fraction {
}
}
}

Real khinchin_geometric_mean() const {
if (b_.size() == 1) {
const size_t size_ = b_.size();
if (size_ == 1) {
return std::numeric_limits<Real>::quiet_NaN();
}
using std::log;
using std::exp;
// Precompute the most probable logarithms. See the Gauss-Kuzmin distribution for details.
// Example: b_i = 1 has probability -log_2(3/4) ~ .415:
// A random partial denominator has ~80% chance of being in this table:
const std::array<Real, 7> logs{std::numeric_limits<Real>::quiet_NaN(), Real(0), log(static_cast<Real>(2)), log(static_cast<Real>(3)), log(static_cast<Real>(4)), log(static_cast<Real>(5)), log(static_cast<Real>(6))};
const std::array<Real, 7> logs{std::numeric_limits<Real>::quiet_NaN(), static_cast<Real>(0), log(static_cast<Real>(2)), log(static_cast<Real>(3)), log(static_cast<Real>(4)), log(static_cast<Real>(5)), log(static_cast<Real>(6))};
Real log_prod = 0;
for (size_t i = 1; i < b_.size(); ++i) {
for (size_t i = 1; i < size_; ++i) {
if (b_[i] < static_cast<Z>(logs.size())) {
log_prod += logs[b_[i]];
}
Expand All @@ -119,44 +143,57 @@ class simple_continued_fraction {
log_prod += log(static_cast<Real>(b_[i]));
}
}
log_prod /= (b_.size()-1);
log_prod /= (size_-1);
return exp(log_prod);
}

Real khinchin_harmonic_mean() const {
if (b_.size() == 1) {
const size_t size_ = b_.size();
if (size_ == 1) {
return std::numeric_limits<Real>::quiet_NaN();
}
Real n = b_.size() - 1;
Real n = size_ - 1;
Real denom = 0;
for (size_t i = 1; i < b_.size(); ++i) {
for (size_t i = 1; i < size_; ++i) {
denom += 1/static_cast<Real>(b_[i]);
}
return n/denom;
}


// Note that this also includes the integer part (i.e. all the coefficients)
const std::vector<Z>& partial_denominators() const {
return b_;
}


inline std::vector<Z>&& get_data() noexcept {
return std::move(b_);
}

template<typename T, typename Z2>
friend std::ostream& operator<<(std::ostream& out, simple_continued_fraction<T, Z2>& scf);

private:
const Real x_;
static constexpr int std_precision = std::numeric_limits<Real>::max_digits10;

void canonicalize() {
// Deal with non-uniqueness of continued fractions: [a0; a1, ..., an, 1] = a0; a1, ..., an + 1].
// The shorter representation is considered the canonical representation,
// so if we compute a non-canonical representation, change it to canonical:
if (b_.size() > 2 && b_.back() == 1) {
b_.pop_back();
b_.back() += 1;
}
b_.shrink_to_fit();
}

std::vector<Z> b_;

int precision_{std_precision};
};


template<typename Real, typename Z2>
std::ostream& operator<<(std::ostream& out, simple_continued_fraction<Real, Z2>& scf) {
constexpr const int p = std::numeric_limits<Real>::max_digits10;
if constexpr (p == 2147483647) {
out << std::setprecision(scf.x_.backend().precision());
} else {
out << std::setprecision(p);
}

out << std::setprecision(scf.precision_);
out << "[" << scf.b_.front();
if (scf.b_.size() > 1)
{
Expand All @@ -171,6 +208,41 @@ std::ostream& operator<<(std::ostream& out, simple_continued_fraction<Real, Z2>&
return out;
}

template<typename Real, typename Z = std::int64_t>
inline auto simple_continued_fraction_coefficients(Real x)
{
auto temp = simple_continued_fraction<Real, Z>(x);
return temp.get_data();
}

// Can be used with `boost::rational` from <boost/rational.hpp>
template <typename Rational, typename Real, typename Z = std::int64_t>
inline Rational to_rational(const simple_continued_fraction<Real, Z>& scf)
Comment on lines +218 to +220
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this work with other rational types (e.g. mpq_t)?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume that mpq_t cannot be used since it is C-style struct which requires to call mpq_init etc.
The function can be used with any type T that supports typename T::int_type, T(typename T::int_type) (just integer part) and T(typename T::int_type, typename T::int_type) (numerator and denominator).
I assume C++20 is not supported so I do not use concepts or such.

{
using int_t = typename Rational::int_type;

auto& coefs = scf.partial_denominators();
const size_t size_ = coefs.size();
assert(size_ >= 1);
if (size_ == 1) return static_cast<int_t>(coefs[0]);

// p0 = a0, p1 = a1.a0 + 1, pn = an.pn-1 + pn-2 for 2 <= n
// q0 = 1, q1 = a1, qn = an.qn-1 + qn-2 for 2 <= n

int_t p0 = coefs[0];
int_t p1 = p0*coefs[1] + 1;
int_t q0 = 1;
int_t q1 = coefs[1];
for (size_t i = 2; i < size_; ++i) {
const Z cn = coefs[i];
const int_t pn = cn*p1 + p0;
const int_t qn = cn*q1 + q0;
p0 = std::exchange(p1, pn);
q0 = std::exchange(q1, qn);
}

return {p1, q1};
}

}
#endif
37 changes: 37 additions & 0 deletions test/simple_continued_fraction_test.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/*
* Copyright Nick Thompson, 2020
* Copyright Matt Borland, 2023
* Use, modification and distribution are subject to the
* Boost Software License, Version 1.0. (See accompanying file
* LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
Expand Down Expand Up @@ -131,6 +132,37 @@ void test_khinchin()
CHECK_ULP_CLOSE(Real(2), Km1, 10);
}

template <typename Real>
void test_git_issue_970()
{
using boost::math::tools::simple_continued_fraction_coefficients;

auto coefs1 = simple_continued_fraction_coefficients(static_cast<Real>(3.14155L)); // [3; 7, 15, 2, 7, 1, 4, 2]
auto coefs2 = simple_continued_fraction_coefficients(static_cast<Real>(3.14165L)); // [3; 7, 16, 1, 3, 4, 2, 4]

const std::size_t max_size = (std::min)(coefs1.size(), coefs2.size());
std::vector<std::int64_t> coefs;
coefs.reserve(max_size);

for (std::size_t i = 0; i < max_size; ++i)
{
const auto c1 = coefs1[i];
const auto c2 = coefs2[i];
if (c1 == c2)
{
coefs.emplace_back(c1);
continue;
}

coefs.emplace_back((std::min)(c1, c2) + 1);
break;
}

// Result is [3; 7, 16]
CHECK_EQUAL(coefs[0], static_cast<std::int64_t>(3));
CHECK_EQUAL(coefs[1], static_cast<std::int64_t>(7));
CHECK_EQUAL(coefs[2], static_cast<std::int64_t>(16));
}

int main()
{
Expand All @@ -157,5 +189,10 @@ int main()
test_simple<float128>();
test_khinchin<float128>();
#endif

test_git_issue_970<float>();
test_git_issue_970<double>();
test_git_issue_970<long double>();

return boost::math::test::report_errors();
}