Module fraction::approx::sqrt

source ·
Expand description

Implements an arbitrary-precision square-root approximation algorithm.

This module extends GenericFraction with eight (8) methods:

All of these methods return some form of an approximation for the square root, and they all use the same algorithm but behave slightly differently.

A note on performance: These methods are designed to be fast in release builds. An unfortunate side effect of this is that performance is significantly degraded when compiler optimisations are disabled. Please consider enabling compiler optimisations in your project if you wish to use these methods.

§Accuracy of the output

The behaviour detailed in this section is common to all methods.

The algorithm in use is iterative: an initial estimate is generated, and this is repeatedly refined until it is suitably accurate. Of course, it is not possible to check the approximation against the true square root value, because we don’t know it. (If we did, we’d just return that instead.) Because of this, the “closeness” must be determined by squaring the approximation to see how close it comes to the original value.

For example, let’s say we wanted to find the square root of 0.0152399025 (6095961/400000000). We might guess 0.1. To check how close we are, we square our guess of 0.1 and get 0.01. This square is accurate to 2 decimal places (0, 1). We might guess again, this time with 0.12. This gives us 0.0144, so we’re still only accurate to 2 decimal places. Guessing 0.123 yields a square of 0.015129, which is now accurate to 3 decimal places.

The above is the checking process that happens when you call any of the methods defined in this module. You must supply a value denoting how accurate you want the square of the approximation to be. It is very important to realise that this is not the same as the accuracy of the square root! However, for x > 1, x.sqrt(n) where n is the number of decimal places that the square must be accurate to will actually produce a square root which is accurate to more than n decimal places. In other words: as long as you aren’t using small numbers, you can consider n to be a lower bound on the accuracy of the square root itself. When you are working with numbers less than 1, this is no longer the case, and the square root may be less accurate than the square (although this is still not typically the case). In all cases, the square of the result you get will be accurate to the level of accuracy you gave to the method.

Due to the nature of the algorithm, most outputs will actually be vastly more accurate than requested. This should not be relied on, and is subject to change. In some instances, the result will actually be exactly correct (e.g. for small square numbers like 4, 9, 16, 25, etc.).

§Method variants

§raw

raw methods return the approximation in unsimplified form via RawApprox, which can (in most cases; see its documentation for more details) be destructured into a Ratio. These methods are typically faster than non-raw ones by an order of magnitude or more, especially for extreme inputs (very large or very small). Do not use raw methods unless you are writing very performance-critical code: most arithmetic operations on Ratio and GenericFraction will automatically simplify the result so although the sqrt_raw method will still be faster than the non-raw version, you will still incur the overhead of simplification, just somewhere else instead.

// We save time here by using `raw`...
match x.sqrt_raw(10_000) {
    RawApprox::Rational(ratio) => {
        // ...but we lose the time again here, because the `+` simplifies the ratio.
        let _ = ratio + Ratio::<BigUint>::zero();
    },

    _ => (),
}

Be aware that unsimplified values can take up excessive amounts of memory (100 KB and beyond, particularly when high levels of accuracy are used).

For a few (limited) examples of how you can write performance-critical code with Ratio and GenericFraction, see this module’s own implementation.

§with_accuracy

These methods accept an Accuracy value instead of a u32 representing a number of decimal places. It is recommended that you use with_accuracy whenever you need to approximate more than one square root to the same level of accuracy. This avoids unnecessary heap allocations.

// Don't do this! `sqrt` allocates a new `Accuracy` each time.
for n in 0..100 {
    let x: GenericFraction<u8> = n.into();
    some_fn(x.sqrt(1_000));
}

// Instead, do this:
let accuracy = Accuracy::decimal_places(1_000_u32);

for n in 0..100 {
    let x: GenericFraction<u8> = n.into();
    some_fn(x.sqrt_with_accuracy(&accuracy));
}

If you only perform one sqrt computation, there is no benefit to using sqrt_with_accuracy.

Keep in mind that Accuracy has some pre-allocated values which you may wish to use.

§abs

These methods ignore the sign of the value and instead approximate the square root of the magnitude only.

The default behaviour is as follows:

let something_positive: GenericFraction<u8> = 2.into();
let something_negative: GenericFraction<u8> = (-2).into();

// Calling `sqrt` on a negative number will return `NaN`.
assert_eq!(something_negative.sqrt(100), GenericFraction::NaN);

// Calling `sqrt_abs` on a negative number will not return `NaN`.
assert_ne!(something_negative.sqrt_abs(100), GenericFraction::NaN);

// `sqrt +2` is equal to `sqrt_abs -2`.
assert_eq!(something_positive.sqrt(100), something_negative.sqrt_abs(100));

The following are guaranteed to hold for any pair of abs and non-abs methods:

// When `x` is positive:
assert_eq!(x.sqrt(100), (-x).sqrt_abs(100));

// When `x` is negative:
assert_eq!((-x).sqrt(100), x.sqrt_abs(100));

Enums§

  • An unsimplified approximation of a square root.