Click here to Skip to main content
15,887,135 members
Articles / General Programming / Algorithms

Practical Implementation of Interval Arithmetic

Rate me:
Please Sign up or sign in to vote.
5.00/5 (5 votes)
16 Mar 2024CPOL25 min read 2.9K   5   7
The steps needed to build an Interval Arithmetic class in C++
Interval arithmetic is a mathematical technique that handles ranges of values rather than precise numbers. Instead of working with a single value, calculations are performed with intervals representing the range of possible values. Each interval has a lower and upper bound, representing the minimum and maximum possible values respectively.

Contents

Introduction

You can find examples of interval arithmetic template classes for the C++ programming language but none that are easy to understand and implement. That is why this paper was written “Practical Implementation of Interval Arithmetic”. The paper highlights implementing a general interval template class supporting the float and double type of the C++ programming language.

The primary advantage of interval arithmetic is its ability to provide guaranteed bounds on results, which is especially useful for dealing with rounding errors, measurement uncertainties, or imprecise inputs. When calculations are performed, the results are also intervals that encompass all possible outcomes. This is particularly valuable in fields like numerical analysis, computer science, and engineering, where understanding the precision and reliability of computations is crucial. It allows for more robust error handling and uncertainty management in complex calculations.

Building an interval template software class that can handle all interval arithmetic for IEEE754 floating point is simple math. This paper describes the practical implementation aspect of building an interval template class and discusses the tricks and math behind the implementation. The implementation spans from the basic operators like +,-,*,/ to elementary functions like sqrt(), log(), pow(), exp() and trigonometric functions of sin(), cos(), tan(), asin(), acos() and atan() and hyperebolic functions of sinh(), cosh(), tanh(), asinh(), acosh(), atanh(). Of course, it also supports interval constants like π, ln2, and ln10. The interval arithmetic supports the basic C++ types of float and double.

As usual, we look at the practical implementation aspect of the template class and not so much the theoretical rationale behind it. See [4] Interval Arithmetic: From Principles to Implementation and [5] for some deeper insight into interval arithmetic and the use of IEEE754 floating point format.

This article will be limited to the phase one implementation. The full source and document can be found on my web site at Interval Arithmetic (hvks.com).

Change Log

  • 13th March, 2024. This is a completely rewritten second version of the original form 2015.

History of Interval Arithmetic

The history of interval arithmetic dates back to the mid-20th century and is intertwined with the development of computer science and numerical analysis. Here’s a brief overview of its evolution:

Early Concepts around the 1950s. The roots of interval arithmetic can be traced to the work of mathematicians like Archimedes. However, the formal development began in the 1950s with the advent of digital computers.

The development of interval arithmetic as a formalized field is largely credited to Ramon E. Moore. Moore, while working at the Applied Physics Laboratory at Johns Hopkins University and later at Stanford University, developed the basic rules and operations of interval arithmetic. His work was aimed at addressing the problem of error bounds in calculations, which was crucial for reliable scientific computing.

Expansion and Formalization of interval arithmetic happens in the 1970s-1980s. The field expanded significantly during this period. Researchers began to formalize the arithmetic and explore its theoretical properties. They also started to apply it to various problems in scientific computing, engineering, and error analysis.

With the rise of computer science, interval arithmetic started to be integrated into computer algorithms and software in the 1980s-1990s time frame. This was facilitated by the increasing need for reliable numerical computations in engineering and the sciences.

After the 2000s Interval arithmetic has continued to grow and find new applications. It’s used in fields ranging from numerical analysis to robotics and control systems. The development of specialized software and hardware to efficiently perform interval arithmetic has also been a focus.

Throughout its history, interval arithmetic has been driven by the need for accurate and reliable numerical computations, especially in the face of uncertain data or rounding errors. Its development reflects a broader trend in mathematics and computer science towards more robust and error-resistant methods. As a curious example

Archimedes is one of the earliest known figures to use a form of interval arithmetic in estimating the value of π. He did this by inscribing and circumscribing polygons around a circle and calculating their perimeters. By increasing the number of sides of the polygons, he was able to narrow the bounds and approximate π more closely. At that time, Archimedes couldn't calculate the exact value of π. He determined that π was between (approximately 3.1429) and (approximately 3.1408). This method of approximation was quite sophisticated for its time and is an early historical example of the principles behind interval arithmetic, where values are confined within an upper and lower bound to provide a range of possible values.

Application for Interval Arithmetic

Interval arithmetic has several practical applications across various fields due to its ability to handle uncertainties and provide guaranteed bounds on calculations. Here are some of its primary applications starting with Numerical Analysis which ensures accuracy and reliability in computations by accounting for rounding errors and other numerical uncertainties.
In rendering computer graphics and geometric computations, interval arithmetic helps in handling inexact data and calculations, leading to more robust algorithms. For systems that require stability and reliability (control systems), interval arithmetic can manage uncertainties in measurements and model parameters. In planning and control of robotics, it’s used to account for the imprecision in sensor data and to ensure safe movements and decisions. In engineering design, Engineers use interval arithmetic for designing systems with tolerances and variations in material properties. For simulations and modeling in scientific computing where input data might be uncertain or vary over a range, interval arithmetic provides a way to ensure results account for all possible input values. This also extends to optimization problems, error analysis, and propagation which helps in understanding how errors in input data can affect the outcome of calculations, which is crucial in sensitive applications like aerospace and medical equipment.

By providing a framework to handle and propagate uncertainties, interval arithmetic enhances the robustness and reliability of computational tasks in these and other areas.

Interval Arithmetic

To bound rounding errors when performing floating point arithmetic, we can use interval arithmetic to keep track of rounding errors. After a series of operations using basic operators like +,-,*, and /, we end with an interval instead of an approximation of the result. The interval width represents the uncertainty of the result but we would know for sure that the correct result will be within this interval.

In interval arithmetic, an interval is typically represented as [a, b], where 'a' is the lower bound and 'b' is the upper bound. Conventionally, it is assumed that a ≤ b, defining what is known as a proper interval. Conversely, if a > b, the interval is termed an improper interval. According to the IEEE 1788 standard for interval arithmetic, both proper and improper intervals are valid and must be correctly handled in computations, as we cannot always assume that a ≤ b. This necessitates determining the lower bound (infimum) of an interval by taking min(a, b), and the upper bound (supremum) by taking max(a, b). This requirement introduces additional steps in the implementation: rather than assuming a ≤ b, code must incorporate functions to find the min and max values for any given interval.

Additionally, an interval where a = b is known as a singleton interval.

For simplicity in formulas and explanations, we often present intervals as if they are proper (a ≤ b). However, in practice, especially in coding implementations, it's crucial to handle intervals correctly according to their proper or improper nature (a>b).

Moreover, it's important to define closed intervals [a, b], semi-closed (or semi-open) intervals (e.g., [a, b) or (a, b]), and open intervals (a, b). Each of these types has different implications for the inclusion or exclusion of the endpoint values in the interval.

The five different interval types are:

Interval Type Definition Symbol Comments
Close a≤x≤b [a,b]  
Left Open a<x≤b (a,b] Same as right close
Right Open a≤x<b [a,b) Same as left close
Open a<x<b (a,b)  
Empty   Empty Interval

Besides the ∅ (empty interval), the 4 other different types are not strictly needed by the standard. However, it is quite convenient to have them otherwise defined an open or semi-open interval will be challenged. Example, creating an open interval (0,1) will be something like this in C++:

C++
interval<double> i(nextafter(0,+INFINITY), nextafter(1,-INFINITY));

A rather inconvenient and cumbersome way of creating this interval while in my implementation can be done like this:

C++
interval<double> i(0,1,OPEN);

This approach ensures a comprehensive understanding and correct implementation of interval arithmetic, adhering to the IEEE 1788 standard.

An interval is presented by two numbers representing the lower and upper range of the interval:

[a,b] Where Image 1 (for proper intervals))

The 4 basic operators +,-,*, and / for a proper interval are then defined by:

[a,b] + [c,d] = [a+c,b+d]
[a,b] – [c,d] = [a-d, b-c]
[a,b] * [c,d] = [min(ac,ad,bc,bd),max(ac,ad,bc,bd)]
[a,b] / [c,d] = [min( and [c,d] does not contain 0.

The division can also be rewritten to:

[a,b] / [c,d] = [a,b] * (1/[c,d]) = [a,b] * [1/d,1/c]

Now what we don’t like is the extra work we would need to do for multiplication of intervals. According to the definition, it will require 8 multiplications to do an interval multiplication. We can however get by with a lot less.

Let’s first define an interval class as follows:

Class if [a,b] Conditions a≤b
Zero a=0 ^ b=0
Mixed a<0 ^ b≥0
Positive a>0
Negative b<0

Instead of the 8 multiplications, we can get by with 2 in most cases and 4 multiplications in worst case based on the following table:

Class of [a,b] Class of [c,d] Multiplication
Positive Positive [a*c,b*d]
Positive Mixed [b*c,b*d]
Positive Negative [b*c,a*d]
Mixed Positive [a*d,b*d]
Mixed Mixed [min(a*d,b*c),max(a*c,b*d)]
Mixed Negative [b*c,a*c]
Negative Positive [a*d,b*c]
Negative Mixed [a*d,a*c]
Negative Negative [b*d,a*c]
Zero - [0,0]
- Zero [0,0]

From a practical standpoint, we can’t just use the above definition “as is”. When performing any basic operations, we will always introduce some rounding errors when using a finite representation like the IEEE754 standard for 32-bit binary floating-point arithmetic and 64-bit binary floating-point arithmetic which is common in most programming languages and CPUs from most vendors like Intel, AMS, etc.

General speaking when performing any of the basic operations like +,-,*,/ we have an error that is bound by the ½β1-t using the default rounding mode in IEEE754 or when rounding towards positive infinity or negative infinity. In IEEE754 binary arithmetic β=2 and t=24 (23+1) for 32-bit float and t=53 (52+1) for 64-bit float, this gives a relative error when performing any of the basic operations

IEEE754 Rounding Towards +∞ or -∞
32-bits float ~5.96-8 ~1.19-7
64-bits float ~2.22-16 ~1.11-16

When performing the interval operations, we would need to also control the rounding mode to ensure that the result is really within the interval. Image 2 means rounding towards -∞ and Image 3 means rounding towards +∞

Image 4

Image 5

Image 6

Image 7

From an IEEE1788 standard, it is also required that an interval can be empty. It is designated with the symbol ∅. There are a few rules for the empty interval.

[a,b]+∅=[a,b], same goes for ∅+[a,b]:=[a,b]
[a,b]-∅:=[a,b], ∅-[a,b]:=[-b,-a]
[a,b]*∅:=∅, ∅*[a,b]:=∅
[a,b]/∅:=∅, ∅/[a,b]:=∅

Lastly, we also need to be aware of what happens when you square an interval e.g. [a,b]2. If you take the interval [-1,1] and just multiply it with itself using the multiplication formula you get a new interval of [-1,1] which is unnecessarily large. If you look at the plot of x2 in the range [-1,1] you get:

Image 8

As you can see the result goes from +1 down to 0 and then rises again to +1. The right result for such an operation is [0,1]. The IEEE 1788 standard kind of foreseen this issue and required a compliant implementation to have a function called sqr() for the squaring of intervals. This issue persists for even power of an interval which needs to be handled properly in an implementation.

If you look at the plot for x3, you get:

Image 9

As you can see, the value of the operation runs through the interval of [-1,1].

Controlling the Rounding

The default rounding mode in the IEEE754 floating-point standard is round to nearest (RN for short). However, as shown in the previous section, we need access to round down towards -∞ (RD) and round up towards +∞ (RU).

In interval arithmetic, rounding is a crucial aspect, and it can be implemented either using hardware-based methods or software-based emulation. Hardware-based rounding offers speed and accuracy but lacks flexibility and portability. In contrast, software emulation is more portable and flexible but can be slower and potentially less accurate.

Hardware-Based Rounding

Here are some of the pros of using hardware-based rounding.
Hardware-based rounding is typically faster as it utilizes the processor’s built-in capabilities. It often offers more precise and consistent rounding due to the direct implementation in the CPU. Reduces the overhead of software emulation, making computations more efficient.

However, the advantages in speed come at a cost, since it relies on specific hardware features, which might not be available or consistent across different platforms or processors.

Hardware implementations are harder to customize or update compared to software. Software relying on specific hardware features may face portability issues across different systems.

As required in IEEE754 round to -∞ (RD) and round to +∞ (RU) are available and it is just a simple matter to switch between these modes from the default round to the nearest.

Controlling Hardware Rounding

To control the rounding process, the IEEE754 offers at least 4 ways of rounding.

  1. Rounding to the nearest which is the default rounding method
  2. Rounding down towards -∞
  3. Rounding up towards +∞
  4. Rounding towards zero (truncating)

Depending on the CPU architecture different instructions enabled these different modes. However, it is unfortunately not portable from architecture to architecture. We will just rely on that somehow the following three functions are available that enable these modes.

C++
Fp_near();       // Set Floating-point mode to round to the nearest

Fp_Down();       // Set floating-point mode to round down towards -∞

Fp_up();         // Set floating-point mode to round towards +∞

As an example of how it can be used. We can implement the interval addition of two intervals [a,b] and [c,d] as follows.

C++
Fp_up();
f=b+d;
Fp_down();
e=a+c;
Fp_near();
return [e,f];

It seems advantageous to use hardware-controlled rounding, however, in [7], they noted that switching back and forth between the different hardware rounding modes is a relatively expensive operation and surprisingly it turns out that using the software rounding method provides a higher performance in many cases. Although it depends highly on the actual CPU architecture.

Software-Based Rounding (Emulation)

Software-based rounding for sure has its advantages. Software-based rounding is more portable as it does not depend on specific hardware features. It is easier to customize or adapt to specific needs, such as implementing non-standard rounding modes and it ensures consistent behavior across different platforms and hardware.

The drawback of software-based rounding is that it is typically slower than hardware implementations due to the overhead of emulation and it can be more complex to implement and maintain its accuracy and consistency compared to hardware implementations, depending on the quality of the software emulation.

The software-based rounding takes advantage of round to nearest is the default rounding mode in IEEE754. and two algorithms are useful. The two-sum and two-product algorithms are fundamental in interval arithmetic for maintaining precise bounds. These operations are essential for addressing the problem of rounding errors in floating-point arithmetic. see [7]

  1. The Two-Sum Algorithm is used to add two floating-point numbers while capturing the error in the result due to rounding. The two-sum operation returns two numbers: the exact sum (as close as the floating-point representation allows) and the error.
  2. The Two-Product Algorithm is similar to the two-sum, the two-product algorithm multiplies two floating-point numbers, providing the product and the error.

To see how we can take advantage of the two algorithms, let's consider the sum of the arithmetic operation a+b. The result will be rounded to the nearest result (RN). The round to the nearest is either the same as the round down (RD) or the same as rounded up (RU). To figure out which we use the error from the two-sum algorithm. If the error is negative, then:

RN(a+b)==RU(a+b) and RD(a+b) is predecessor(RN(a+b))

If e is positive then:

RN(a+b)==RD(a+b) and RU(a+b) is successor(RN(a+b))

The successor returns the next representable floating-point number towards +∞ and the predecessor returns the next representable floating-point number towards -∞.

The same analogy can be used for multiplication. If the error is negative then:

RN(a*b)==RU(a*b) and RD(a*b) is predecessor(RN(a*b))

If e is positive then:

RN(a*b)==RD(a*b) and RU(a*b) is successor(RN(a*b))

You can extend that to division, reciprocal, and square root, see the table below.

Operation Error<0 Error>0
RN(a+b) RU=RN, RD=predecessor RD=RN, RU=successor
RN(a-b) RU=RN, RD=predecessor RD=RN, RU=successor
RN(a*b) RU=RN, RD=predecessor RD=RN, RU=successor
RN(a/b) RU=RN, RD=predecessor RD=RN, RU=successor
RN(1/b) RU=RN, RD=predecessor RD=RN, RU=successor
RN(sqrt(a)) RU=RN, RD=predecessor RD=RN, RU=successor

Two-sum algorithm was invented in 1965 by O. Møller and comes in two variations. The two-sum and the fast two-sum algorithm and implementation of this are below.

The original two-sum algorithm in C++.

C++
//  Implement the twosum algorithm. Assuming round to nearest mode
//  (default IEEE754)
//  sum=(a+b)
//  a1=sum-b
//  b1=sum-a
//  da=a-a1
//  db=b-b1
//  err=da+db
//  return sum, err
//  if err is negative then sum=Round_up(a+b),
//  and round_down(a+b)=previous(sum)
//  if err is positive then sum=Round_down(a+b),
//  and Round_up(a+b)=successor(sum)
//  The two_sum algorithm requires 6 floating point operations
std::pair<_IT, _IT> two_sum(_IT a, _IT b)
{
       const _IT sum = (a + b);
       const _IT a1 = sum - b;
       const _IT b1 = sum - a;
       const _IT da = a - a1;
       const _IT db = b - b1;
       const _IT err = da + db;

       return std::make_pair(sum, err);
}

This algorithm requires approximately 6 floating-point operations.

A faster version, surprisingly called the fast two-sum algorithm is shown below in C++.

C++
//  Implement the fast two-sum algorithm Assuming round to nearest mode (default IEEE754)
//  sum=(a+b)
//  a1=sum-a
//  err=b-a1
//  return sum, err
//  if err is negative then sum=Round_up(a+b),
//  and round_down(a+b)=previous(sum)
//  if err is positive then sum=Round_down(a+b),
//  and Round_up(a+b)=successor(sum)
//  The fast two-sum algorithm requires only 3 floating point instructions
//  and a comparison
std::pair<_IT, _IT> fasttwo_sum(_IT a, _IT b)
{
       const _IT sum = a + b;
       _IT err, tmp;
       if (abs(a) > abs(b))
       {
             tmp = sum - a; err = b - tmp;
       }
       else
       {
             tmp = sum - b; err = a - tmp;
       }
       return std::make_pair(sum, err);
}

which only need 3 floating point operations and comparison operations. However, it requires that a≥b for the sum of a+b.

The two products were invented by Rump, see C++ implementation below.

C++
// Split argument into a high and low. (Dekker's method)
// double has 53bits in mantissa(52+1) and shifting is therefore (53+1)/2=27
// float has 24bits in mantissa (23+1) and shifting is therefore (23+1)/2=12
_IT split(_IT a)
{
       const _IT C = a*double((1 << 27) + 1);
       _IT xhigh, xlow;
       xhigh = C - (C - a);
       xlow = a - xhigh;
       return std::make_pair(xhigh, xlow);
}

// The standard two-product algorithm (by RUMP's method)
// (xh,xl)=split(a)
// (yh,yl)=split(b)
// p=a*b
// t1=-p+xh*yh
// t2=t1+xh*yl
// t3=t2+xl*yh
// err=t3+xl*yl
// return (p,err)
//     if err is negative then sum=Round_up(a*b),
//     and round_down(a*b)=previous(sum)
//     if err is positive then sum=Round_down(a*b),
//     and Round_up(a*b)=successor(sum)
// The fast two-product algorithm requires 17 floating-point instructions
// and a comparison
std::pair<_IT, _IT> two_prod(_IT a, _IT b)
{
       const std::pair<_IT, _IT> x= split(a);
       const std::pair<_IT, _IT> y = split(b);
       const _IT p = a * b;
       const _IT t1 = -p + x.first * y.first;
       const _IT t2 = t1 + x.first * y.second;
       const _IT t3 = t2 + x.second * y.first;
       const _IT err = t3 + x.second * y.second;
       if (abs(p) > ldexp(1, -1021))  // avoiding overflow
             err = x.second * y.second - ((((p * 0.5 - (x.first * 0.5) * y.first) * 2)
                   -x.second * y.first) - x.first * y.second);
       else
             err=x.second*y.second-(((p-x.first*y.first)-x.second*y.first)
                 -x.first*y.second);
       return std::make_pair(p,err);
}

However, as pointed out by [7], if you have access to a fma operation (Fused-Multiply-Add), you can simplify the two-product codes to simple:

The two-product using fma:

C++
//     The fast two product algorithm
//     p=a*b
//     err=fma(a,b,-p) // fma is required in IEEE754 and either implemented 
//     in hardware or software
//     return (p,err)
//     if err is negative then sum=Round_up(a*b),
//     and round_down(a*b)=previous(sum)
//     if err is positive then sum=Round_down(a*b),
//     and Round_up(a*b)=successor(sum)
// The fast two-product algorithm requires only 2 floating point instructions
// and a comparison
std::pair<_IT, _IT> fasttwo_prod(_IT a, _IT b)
{
       const _IT p = a * b;
       const _IT err = fma(a, b, -p);
       return std::make_pair(p, err);
}

Software libraries and systems that support interval arithmetic often implement these algorithms to ensure that all possible values within the intervals are accurately accounted for. Some well-known software and libraries include INTLAB: A MATLAB toolbox for interval arithmetic that provides reliable computations with rounding error bounds. The Boost Interval Arithmetic Library: Part of the Boost C++ Libraries, it supports interval arithmetic operations with various policies for controlling the rounding and handling of interval bounds.

These tools and libraries are used in various fields, from scientific computing to engineering and finance, wherever accurate error tracking and robust interval calculations are needed. They implement not just two-sum and two-products but a whole range of arithmetic and mathematical operations adapted for interval arithmetic.

Interval Class Requirements

With the definition of interval arithmetic done and the two crucial algorithms two-sum and two-product, we can now turn to the requirement of an interval class.

It is always important to have your requirements ready before starting the C++ coding process. I usually divide it up into phases so I start with a base implementation and then expand it further through the defined phases. For each phase, you add testing of the phase functionalities.

Phase 1: establishing the base functionality
All the essential operators: +,-,*,/,=,+=,-=,*=,/=.
Constructors for a float, double, or long double to interval type.
Conversion operators from interval to regular float, double, long double, short, int, long.
Support of closed, semi-closed, semi-open, and open intervals.
Support the empty interval set ∅.
cout and cin operator.
Comparison operators ==,!=,<,<=,>,>=.
Essentials methods:
            negate: Negate the interval (same as monadic -).
            toString: return the string representation of an interval number.
            width: return the width of the interval. (standard names this wid).
            center: return the center of the interval (standard names this mid).
            radius: return the radius of the interval (standard names this rad).
            infimum: return the infimum of the interval (standard name it inf).
            supremum: return the supremum of the interval. (standard name it sup).
            leftinterval: return or set the lower range of the interval as is.
            rightinterval: return or set the upper range of the interval as is.
            isEmpty: return the Boolean value of emptiness of the interval.
            isProper: return true if the interval is a proper interval otherwise false.
            isImproper: return true if the interval is an improper interval otherwise false.
            isPoint: return true if the interval is a singleton, otherwise false.
            isClass: return the classification of the interval.
           abs: return the absolute value of the interval.
           sqrt: return the square root of the interval.
           sqr: return the square of the interval (x2).

Phase 2: expanding the base functionality
mixed data type for arithmetic operations.
manifest interval constants: LN2, LN10, e, PI
exponential (exp) and logarithm functions. (log,log10)
power x<sup>y</sup>
Union: Union of two intervals
Intersection: intersections of two intervals
Interior: is the interval an interior to another interval
Precedes: Does the interval precede another interval
Subset: is the interval a subset of another interval
Inclusion: is the interval included in another interval
In: is the float or double number within the interval

Phase 3: Completed all the auxiliary functions
Trigonometric interval functions
Hyperbolic interval functions:

Phase 4:
Expand support for arbitrary precision interval type
other useful functions e.g. gamma, beta, etc.

Phase 1 Implementation

Is all about establishing the base functionality of the interval class.

Defining the Interval Template Class

With the implementation split into phases, we are now ready to begin our initial design.
We need to create a template class for our interval functionality. In essence, similar to the standard complex template library in C++. Also, the class needs to hold the left and right sides of the interval plus the interval type to be able to cope with Closed, Semi-open, or fully open intervals as previously discussed. Not surprisingly, we end up with this template class definition and the private parts and variables.

C++
// Interval class
// Realistically the class Type can be float, or double.
// Any other type is not supported
//
template<class _IT> class interval {
       _IT left;                  // Left interval
       _IT right;                 // Right interval
       enum interval_type type;   // Interval type CLOSE, OPEN, LEFT_OPEN,
                                    RIGHT_OPEN, (RIGHT_CLOSE is synonym for
                                    LEFT_OPEN and LEFT_CLOSE same as RIGHT_OPEN
       public:

   };

We have also established our two functions, fasttwo_sum() and fasttwo_prod() as internal private functions to the class and we get the following:

C++
// Interval class
// Realistically the class Type can be float, or double.
// Any other type is not supported
//
template<class _IT> class interval {
       _IT left;                  // Left interval
       _IT right;                 // Right interval
       enum interval_type type;   // Interval type CLOSE, OPEN, LEFT_OPEN,
                                  // RIGHT_OPEN, (RIGHT_CLOSE is synonym for
                                  // LEFT_OPEN and LEFT_CLOSE same as RIGHT_OPEN
                                  // and EMPTY

       // Split argument into a right and left. (Dekker's method)
       // double has 53bits (52+1) in mantissa and shifting is
       // therefore (53+1)/2=27
       // float has 24bits (23+1) in mantissa and shifting is
       // therefore (24+1)/2=12
       _IT split(_IT a)
       {
             const _IT C = a * double((1 << 27) + 1);
             _IT xright, xleft;
             xright = C - (C - a);
             xleft = a - xright;
             return std::make_pair(xright, xleft);
       }

       //     Implement the fast two-sum algorithm Assuming round to nearest
       //     mode (default IEEE754)
       //     sum=(a+b)
       //     a1=sum-a
       //     err=b-a1
       //     return sum, err
       //     if err is negative then sum=Round_up(a+b), and
       //     round_down(a+b)=previous(sum)
       //     if err is positive then sum=Round_down(a+b), and
       //     Round_up(a+b)=succesor(sum)
       //     The fast two-sum algorithm requires only 3 floating point
       //     instructions and a comparison
       static std::pair<_IT, _IT> fasttwo_sum(_IT a, _IT b)
       {
             const _IT sum = a + b;
             _IT err, tmp;
             if (abs(a) > abs(b))
             {
                    tmp = sum - a; err = b - tmp;
             }
             else
             {
                    tmp = sum - b; err = a - tmp;
             }
             return std::make_pair(sum, err);
       }

       //     The fast two-product algorithm
       //     p=a*b
       //     err=fma(a,b,-p)     fma is required in IEEE754 and either
       //     implemented in hardware or software
       //     return (p,err)
       //     if err is negative then sum=Round_up(a*b), and
       //     round_down(a*b)=previous(sum)
       //     if err is positive then sum=Round_down(a*b), and
       //     Round_up(a*b)=succesor(sum)
       //     The fast two-product algorithm requires only 2 floating-point
       //     instructions and a comparison
       std::pair<_IT, _IT> fasttwo_prod(_IT a, _IT b)
       {
             const _IT p = a * b;
             const _IT err = fma(a, b, -p);
             return std::make_pair(p, err);
       }

public:

   };

That was the private part of our template class. Now to the constructors for the class.

Interval Constructors

As our requirement goes, our interval class can be declared with zero, one, two, or three parameters. Like in:

C++
// Declare an interval variable with an empty class ∅
Interval<double> i;

// Declare an interval variable initialized with a point
Interval<float> fi(2.0)

// Declare an interval variable as the interval [1,2]
Interval<double> di(1.0,2.0);

// Declare an interval variable with the open interval (3,4)
Interval<double> dit(3.0,4.0,OPEN);

// Declare an improper closed interval variable [3,2]
Interval<float> dit2(3,2);
C++
// constructor. zero, one or two arguments for type _IT
interval();                    // Empty interval
interval(const _IT& d);    // Singleton interval
// Regular interval with interval_type (default CLOSE)
interval(const _IT& l, const _IT& h, const enum interval_type t=CLOSE);

The interval type is defined below:

C++
// The five different interval types
// # Close   a<=x<=b        [a,b]
// # Left open a<x<=b      (a,b]  same as Right close
// # Right open a<=x<b     [a,b)  same as Left close
// # Open a<x<b            (a,b)
// # EMPTY interval        ∅
enum interval_type { EMPTY, CLOSE, LEFT_OPEN, RIGHT_OPEN, OPEN,
                     RIGHT_CLOSE=LEFT_OPEN, LEFT_CLOSE=RIGHT_OPEN };

Interval Coordinate functions

We would also need some coordinating functions. Example, get the left or right value of the interval or change them for that matter. Likely we need to find the width (wid) of the interval, the radius (rad) of the interval, and the midpoint (mid) for converting an interval to a scalar element and we also need to define the required infimum (inf) and supremum (sup) plus the “is” method functions as required by the IEEE 1788 standard.

We also need some direct method to get and set the left and right interval plus getting or setting the interval type ([], (], [), []) and I use leftinterval() and rightinterval() for the interval ends:

C++
// Coordinate functions.
_IT rightinterval() const;                   // Return rightinterval bound
_IT leftinterval() const;                    // Return leftinterval bound
_IT rightinterval(const _IT&);               // Set and return rightinterval bound
_IT leftinterval(const _IT&);                // Set and return leftinterval bound
enum interval_type intervaltype() const;     // Return interval type
enum interval_type intervaltype(enum interval_type); // Set and return interval type

// IEEE1788 standard functions
_IT inf() const;    // Return infimum of interval
_IT sup() const;    // Return supremum of interval
_IT mid() const;    // Return midpoint of interval
_IT rad() const;    // Return radius of interval
_IT wid() const;    // Return width of interval
_IT mig() const;    // Return Mignitude. inf(|x|)
_IT mag() const;    // Return Magnitude. sup(|x|)

// Is methods as required per IEEE 1788 standard
bool isEmpty() const;
bool isPoint() const;
bool isImproper() const;
bool isProper() const;

Previously, I introduced the term interval class as a way to classify a given interval type. Example, zero, positive, negative, mixed, etc. This can also be a helpful function to support. And the toString() function that generates a string representation of the interval.

C++
// Miscellaneous but useful coordinate functions
enum int_class isClass() const;
std::string toString() const;   // Convert interval to a string

the enumerated type int_class is defined as follows:

C++
// The eight different interval classification
// # ZERO           a=0 && b=0
// # POSITIVE0      a==0 && b>0
// # POSITIVE1      a>0 && b>0
// # POSITIVE       a>=0 && b>0
// # NEGATIVE0      a<0 && b==0
// # NEGATIVE1      a<0 && b<0
// # NEGATIVE       a<0 && b<=0
// # MIXED          a<0 && b>0
enum int_class { NO_CLASS, ZERO, POSITIVE0, POSITIVE1, POSITIVE, NEGATIVE0,
                NEGATIVE1, NEGATIVE, MIXED };

Interval Conversion Operators

Is all about defining conversion operators to convert an interval to any of the built-in types in C++.

C++
// Conversion Operators
operator short() const                   // Conversion to short
operator int() const                     // Conversion to int
operator long() const                    // Conversion to long
operator long long() const               // Conversion to long
operator unsigned short() const          // Conversion to unsigned short
operator unsigned int() const            // Conversion to unsigned int
operator unsigned long() const           // Conversion to unsigned long
operator unsigned long long() const      // Conversion to unsigned long
operator double() const;                 // Conversion to double
operator float() const;                  // Conversion to float

Interval Essential Operators

This is all the C++ assignments operators, see below.

C++
// Essential operators
interval<_IT>& operator= ( const interval<_IT>& );
interval<_IT>& operator+=( const interval<_IT>& );
interval<_IT>& operator-=( const interval<_IT>& );
interval<_IT>& operator*=( const interval<_IT>& );
interval<_IT>& operator/=( const interval<_IT>& );

Interval Non-Essential Operators

These are the +,-,*,/ and the monadic version of +,- plus the Boolean ==,!=,>,<,>=,<= operators.

C++
// Arithmetic +,-,*,/ Binary and Unary
template<class _IT> interval<_IT> operator+( const interval<_IT>&, const interval<_IT>& );
template<class _IT> interval<_IT> operator+( const interval<_IT>& );   // Unary
template<class _IT> interval<_IT> operator-( const interval<_IT>&, const interval<_IT>& );
template<class _IT> interval<_IT> operator-( const interval<_IT>& );   // Unary
template<class _IT> interval<_IT> operator*(const interval<_IT>&, const interval<_IT>&);
template<class _IT> interval<_IT> operator/( const interval<_IT>&, const interval<_IT>& );

with the class declaration defined and the outer declaration in place, we can now move to the implementation of it.

Implementing the Interval Class

Most of the implementation is simple and follows the class definition.

  1. Constructors
  2. Coordinate methods
  3. Conversion operators
  4. Essential operators

Implementing the Constructors

The constructors are simple and just populate the interval class with initial values. If no parameters are given, the interval will be initialized as an EMPTY (∅) interval. Notice that for the three arguments constructor, the third argument is defaulted to a closed interval, if not present.

C++
// EMPTY interval
template<class _IT> inline  interval<_IT>::interval()
{ left = _IT(0); right = _IT(0); type = EMPTY; }

// Singleton interval
template<class _IT> inline  interval<_IT>::interval(const _IT& d)
{ left = _IT(d); right = _IT(d); type = CLOSE; }

// Regular interval with optional interval type argument
template<class _IT> inline  interval<_IT>::interval
        (const _IT& l, const _IT& h, const enum interval_type t)
{ left = l; right = h; type = t; }

Implementing the Coordinate Functions (Methods)

These are all the methods as defined in the interval class. Most of these methods are relatively easy to implement. For the methods, we have:

  • rightinterval()
  • leftinterval()
  • intervaltype()

These are simple functions that just return the left, right or type of interval. There is no computation needed and it just returns the current values. There is also an overloaded version that allows you to set these private variables. There is also the isClass() and toString() methods.

The next group is the required IEEE1788 standard functions:

  • inf()
  • sup()
  • mid()
  • rad()
  • wid()
  • mig()
  • mag()
  • isEmpty()
  • isPoint()
  • isImproper()
  • isProper()

Two methods inf() and sup() requires a little explanation.

The inf() function stands for "infimum" or "greatest lower bound (GLB)." It returns the lowest possible value in an interval, effectively representing the lower bound of the interval. This function is crucial for ensuring that any operations or comparisons involving intervals take into account the lowest possible value that an element of the interval can assume, thus maintaining accuracy and reliability in mathematical computations. This mean that regardless of whether the interval is proper (left≤right) or improper (left>right), it still returns the infimum of the interval. This is also valid regardless of whether the interval is closed, open, or semi-open.

The sup() function stands for "supremum". It returns the highest possible value in an interval, effectively representing the upper bound of the interval. Similarly, the sup() function ensures that the highest possible value within an interval is considered during operations or comparisons. This is vital for encompassing the full range of possible values within an interval and for accurate interval arithmetic computations. Again, regardless of whether the interval is proper (left≤right) or improper (left>right), it still returns the infimum of the interval. This is also valid regardless of whether the interval is closed, open, or semi-open

In interval arithmetic, these functions are used to define intervals (e.g., [inf(x), sup(x)] for an interval x) and to perform arithmetic operations between intervals. When operations such as addition, subtraction, multiplication, or division are performed between intervals, the inf() and sup() functions help determine the resulting interval by calculating the new bounds based on the operation performed and in consideration of the interval type and properness of the interval.

C++
// Coordinate functions.
// Return the right interval "as is"
template<class _IT> inline _IT interval<_IT>::rightinterval() const
{ return right; }

// Return the left interval "as is"
template<class _IT> inline _IT interval<_IT>::leftinterval() const
{ return left; }

// Set the right interval "as is".
// If interval is empty set the interval type to CLOSE
template<class _IT> inline _IT interval<_IT>::rightinterval(const _IT& r)
{
       if (type == EMPTY)
             type = CLOSE;
       right = r;
       return right;
}

// Set the left interval "as is".
// If interval is empty set the interval type to CLOSE
template<class _IT> inline _IT interval<_IT>::leftinterval(const _IT& l)
{
       if (type == EMPTY)
             type = CLOSE;
       left = l;
       return left;
}

// Return the current intervaltype
template<class _IT> inline enum interval_type interval<_IT>::intervaltype() const
{ return type; }

// Set the interval type
// The below table is for a proper interval
//                  CLOSE []     OPEN ()      LEFT_OPEN (]       RIGHT_OPEN [)
//     CLOSE  []     #            -,+          -,#           #,+
//     OPEN   ()     +,-          #            #,-           +,#
//     LEFT_OPEN (]  +,#          #,+          #             +,+
//     RIGHT_OPEN [) #,-          -,#          _,_          #
//
//     For improper intervals we preserve the improperness in the result.
//
template<class _IT> inline enum interval_type interval<_IT>::intervaltype
                    (const enum interval_type to)
{
    interval<_IT> x = *this;
    const _IT infi(INFINITY);

    if (x.type != to)
    {
        if (this->isImproper())
        {    // If improper swap the interval and switch the half open intervals
            std::swap(x.left, x.right);
            if (x.type == LEFT_OPEN)
                x.type = RIGHT_OPEN;
            else
                if (x.type == RIGHT_OPEN)
                    x.type = LEFT_OPEN;
        }

        switch (x.type)
        {
        case CLOSE:
            // if the interval is already CLOSE then do nothing
            if (to == LEFT_OPEN || to == OPEN)
                x.left = nextafter(x.left, -infi);
            if (to == RIGHT_OPEN || to == OPEN)
                x.right = nextafter(x.right, +infi);
            break;
        case OPEN:
            // if the interval is already Open then do nothing
            if (to == RIGHT_OPEN || to == CLOSE)
                x.left = nextafter(x.left, +infi);
            if (to == LEFT_OPEN || to == CLOSE)
                x.right = nextafter(x.right, -infi);
            break;
        case LEFT_OPEN:
            // If the interval is already RIGHT_CLOSE same as LEFT_OPEN then do nothing
            if (to == RIGHT_OPEN || to == CLOSE)
                x.left = nextafter(x.left, +infi);
            if (to == RIGHT_OPEN || to == OPEN)
                x.right = nextafter(x.right, +infi);
            break;
        case RIGHT_OPEN:
            // If the interval is already LEFT_CLOSE then do nothing
            if (to == LEFT_OPEN || to == OPEN)
                x.left = nextafter(x.left, -infi);
            if (to == LEFT_OPEN || to == CLOSE)
                x.right = nextafter(x.right, -infi);
            break;
        }
        x.type = to;
        if (this->isImproper())
        {
            std::swap(x.left, x.right);
        }
        *this = x;
    }
    return x.type;
}

// compute the infimum(greatest lower bound) of an interval, 
// taking into account the type of interval
// (closed, open, left-open, or right-open) and whether the interval is proper
// (left endpoint is less than or equal to right endpoint) or improper
// (left endpoint is greater than the right endpoint).
template<class _IT> inline _IT interval<_IT>::inf() const
{
    // For a closed interval, directly return the minimum of left and right.
    if (type == CLOSE)
        return min(left, right);

    _IT adjustedLeft = left;
    _IT adjustedRight = right;
    const _IT infi(INFINITY);

    // Adjust left boundary for open intervals
    if (type == LEFT_OPEN || type == OPEN)
        adjustedLeft = nextafter(left, (left <= right) ? +infi : -infi);

    // Adjust right boundary for open intervals, taking into account improper intervals
    if (type == RIGHT_OPEN || type == OPEN)
        adjustedRight = nextafter(right, (left <= right) ? -infi : +infi);

    // Return the minimum of the adjusted boundaries
    return std::min(adjustedLeft, adjustedRight);
}

// Optimizing the function for calculating the supremum(least upper bound) 
// of an interval follows
// a similar approach to optimizing the infimum calculation
template<class _IT> inline _IT interval<_IT>::sup() const
{
    // For a closed interval, directly return the maximum of left and right.
    if (type == CLOSE)
        return max(left, right);

    _IT adjustedLeft = left;
    _IT adjustedRight = right;
    const _IT infi(INFINITY);

    // Adjust left boundary for open intervals
    if (type == LEFT_OPEN || type == OPEN)
        adjustedLeft = nextafter(left, (left <= right) ? +infi : -infi);

    // Adjust right boundary for open intervals, 
    // considering proper and improper intervals
    if (type == RIGHT_OPEN || type == OPEN)
        adjustedRight = nextafter(right, (left <= right) ? -infi : +infi);

    // Return the maximum of the adjusted boundaries
    return std::max(adjustedLeft, adjustedRight);
}

// Return interval midpoint
template<class _IT> inline _IT interval<_IT>::mid() const
{ if (right == left) return left; else  return (right + left) / _IT(2); }

// Return interval radius
template<class _IT> inline _IT interval<_IT>::rad() const
{ _IT r; r = (right - left) / _IT(2); return r; }

// Return interval width
template<class _IT> inline _IT interval<_IT>::wid() const
{ _IT r; r = right - left; if (r < _IT(0)) r = -r; return r; }

// Return mignitude of class
template<class _IT> inline _IT interval<_IT>::mig() const
{
       _IT l = inf();
       _IT r = sup();
       l = abs(l);
       r = abs(r);
       return std::min(l, r);
}

// Return magnitude of interval
template<class _IT> inline _IT interval<_IT>::mag() const
{
       _IT l = inf();
       _IT r = sup();
       l = abs(l);
       r = abs(r);
       return std::max(l, r);
}

// Required is... methods
template<class _IT> inline bool interval<_IT>::isProper() const
{ return left<=right; }
template<class _IT> inline bool interval<_IT>::isImproper() const
{ return right<left; }
template<class _IT> inline bool interval<_IT>::isPoint() const
{ return left==right; }
template<class _IT> inline bool interval<_IT>::isEmpty() const
{ return type==EMPTY; }

// Return interval classification
template<class _IT> inline enum int_class interval<_IT>::isClass() const
{
       if (left == _IT(0) && right == _IT(0)) return ZERO;
       if (left == _IT(0) && right >  _IT(0)) return POSITIVE0;
       if (left >  _IT(0) && right >  _IT(0)) return POSITIVE1;
       if (left >= _IT(0) && right >  _IT(0)) return POSITIVE;
       if (left <  _IT(0) && right == _IT(0)) return NEGATIVE0;
       if (left <  _IT(0) && right <  _IT(0)) return NEGATIVE1;
       if (left <  _IT(0) && right <= _IT(0)) return NEGATIVE;
       if (left <  _IT(0) && right >  _IT(0)) return MIXED;
       return NO_CLASS;
}

// Return the interval as a String representation
template<class _IT> inline std::string interval<_IT>::toString() const
{
       std::string s;
       enum interval_type t = intervaltype();
       std::ostringstream strs;

       strs.precision(25);
       strs << (t == LEFT_OPEN || t == OPEN ? "(" : "[");
       strs << left;
       strs << ",";
       strs << right;
       strs << (t == RIGHT_OPEN || t == OPEN ? ")" : "]");
       return strs.str();
}

Implementing the Conversion Operators

It is trivial to implement the conversion operators since it just has to return the midpoint of the interval when conversion to any integers type or float and double types. If the interval is empty, the conversion is undefined.

C++
// Conversion Operators
template<class _IT> inline interval<_IT>::operator short() const
{      // Conversion to short
       return static_cast<short>(mid());
}

template<class _IT> inline interval<_IT>::operator int() const
{      // Conversion to int
       return static_cast<int>(mid());
}

template<class _IT> inline interval<_IT>::operator long() const
{      // Conversion to long
       return static_cast<long>(mid());
}

template<class _IT> inline interval<_IT>::operator long long() const
{      // Conversion to long long
       return static_cast<long long>(mid());
}

template<class _IT> inline interval<_IT>::operator unsigned short() const
{      // Conversion to unsigned short
       return static_cast<unsigned short>(mid());
}

template<class _IT> inline interval<_IT>::operator unsigned int() const
{      // Conversion to unsigned int
       return static_cast<unsigned int>(mid());
}

template<class _IT> inline interval<_IT>::operator unsigned long() const
{      // Conversion to unsigned long
       return static_cast<unsigned long>(mid());
}

template<class _IT> inline interval<_IT>::operator unsigned long long() const
{      // Conversion to long long
       return static_cast<unsigned long long>(mid());
}

template<class _IT> inline interval<_IT>::operator double() const
{      // Conversion to double
       return static_cast<double>(mid());
}

template<class _IT> inline interval<_IT>::operator float() const
{      // Conversion to float
       return static_cast<float>(mid());
}

Implementing the Basic Operators

Now that we can control rounding via software emulation, it is pretty straightforward to implement interval arithmetic in a portable way. An assignment is just a copy from the right to the left side of the = operator.

C++
// Assignment operator. Works for all class types
//
template<class _IT> inline interval<_IT>& interval<_IT>::operator=( const interval<_IT>& a )
{
       left = a.left;
       right = a.right;
       type = a.type;
       return *this;
}

Interval Addition

The Interval addition is:

Image 10

Code for addition using the fastwo_sum. Where .inf() returns the lowest number of the interval. And .sup() returns the highest number in the interval.

C++
// += operator. Works for all classes.
// Always return a "proper" and closed [] interval
// a:=a+[EMPTY] or b:=[EMPTY]+b or [EMPTY]:=[EMPTY]+[EMPTY]
template<class _IT> inline interval<_IT>& interval<_IT>::operator+=(const interval<_IT>& a)
{
    // Handle EMPTY interval first
    if (a.type == EMPTY)
        return *this;
    if (type == EMPTY)
        return (*this = a);

    const _IT infi(INFINITY);
    // Neither a or b is [EMPTY]
    std::pair<_IT, _IT> xlow = fasttwo_sum(inf(), a.inf());
    std::pair<_IT, _IT> xright = fasttwo_sum(sup(), a.sup());
    left = xlow.first;
    right = xright.first;
    // Any adjustment?
    if (xlow.second < _IT(0))
        left = nextafter(left, -infi);
    if (xright.second > _IT(0))
        right = nextafter(right, +infi);
    type = CLOSE;
    return *this;
}

Interval Subtraction

The interval subtraction is:

Image 11

Code for subtraction using the fasttwo_sum.

C++
// -= operator. Works for all classes.
// Always return a "proper" and closed [] interval
// a=a-[EMPTY] or -b=[EMPTY]-b or [EMPTY]=[EMPTY]-[EMPTY]
//
template<class _IT> inline interval<_IT>& interval<_IT>::operator-=(const interval<_IT>& a)
{
    // Handle EMPTY interval first
    if (a.type == EMPTY)
        return *this;
    if (type == EMPTY)
        return (*this = -a);

    const _IT infi(INFINITY);
    // Neither a or b is [EMPTY]
    std::pair<_IT, _IT> xlow = fasttwo_sum(inf(), -a.sup());
    std::pair<_IT, _IT> xright = fasttwo_sum(sup(), -a.inf());
    left = xlow.first;
    right = xright.first;
    if (xlow.second < _IT(0))
        left = nextafter(xlow.first, -infi);
    if (xright.second > _IT(0))
        right = nextafter(xright.first, +infi);
    type = CLOSE;
    return *this;
}

Interval Multiplication

The Interval multiplication is:

Image 12

Code for multiplication using the fasttwo_product algorithm and with some optimization.

C++
// Works for all classes.
// [EMPTY]:=a*[EMPTY] or [EMPTY]:=[EMPTY]*b or [EMPTY]:=[EMPTY]*[EMPTY]
// Please note that this is for all integer classes. interval<int>, interval<long>,
// were there is no loss of precision
// Instead of doing the mindless low = MIN(low*a.right, low*a.low,right*a.low,right*a.right) and
// right = MAX(low*a.right, low*a.low,right*a.low,right*a.right) requiring a total of 8 multiplication
//
//   low, right, a.low, a.right    result
//    +     +     -     +        -  +  [ right*a.low, right*a.right ] 2205
//    +     +     -     -        -  -  [ right*a.low, low*a.right ]
//    +     +     +     +        +  +  [ low*a.low, right*a.right ]
//    -     +     +     +        -  +  [ low*a.right, right*a.right ]
//    -     +     -     +        -  +  [ MIN(low*a.right,right*a.low), MAX(low*a.low,right*a.right) ]
//    -     +     -     -        -  -  [ right*a.low, low*a.low ]
//    -     -     +     +        -  -  [ low*a.right, right,a.low ]
//    -     -     -     +        -  -  [ low*a.right, low*a.low ]
//    -     -     -     -        +  +  [ right*a.right, low * a.low ]
//
template<class _IT> inline interval<_IT>& interval<_IT>::operator*=(const interval<_IT>& a)
{
    // Handle EMPTY interval first  ∅
    if (type == EMPTY)
        return *this;
    if (a.type == EMPTY)
        return (*this = a);

    // Neither a or b is ∅
    // Extract intervals through inf() and sup()
    _IT al = inf();
    _IT ah = sup();
    _IT bl = a.inf();
    _IT bh = a.sup();

    auto multiply = [&](const _IT& x, const _IT& y)
    {
        std::pair<_IT, _IT> tmp = interval<_IT>::fasttwo_prod(x, y);
        interval<_IT> res;
        const _IT infi(INFINITY);
        res.left = res.right = tmp.first;
        if (tmp.second < _IT(0))
            res.left = nextafter(tmp.first, -infi);
        if (tmp.second > _IT(0))
            res.right = nextafter(tmp.first, +infi);
        return res;
    };

    interval<_IT> itmp;
    type = CLOSE;
    // Shortcuts
    if (al >= _IT(0) && bl >= _IT(0))
    {// Both intervals positive
        itmp = multiply(al, bl);
        left = itmp.left;
        itmp = multiply(ah, bh);
        right = itmp.right;
        return *this;
    }
    if (ah < _IT(0) && bh < _IT(0))
    {// Both intervals negative
        itmp = multiply(al, bl);
        right = itmp.right;
        itmp = multiply(ah, bh);
        left = itmp.left;
        return *this;
    }
    if (al >= _IT(0) && bh < _IT(0))
    {// [A] interval positive, [B] interval negative
        itmp = multiply(ah, bl);
        left = itmp.left;
        itmp = multiply(al, bh);
        right = itmp.right;
        return *this;
    }
    if (ah < _IT(0) && bl >= _IT(0))
    {// [A] interval negative, [B] interval positive
        itmp = multiply(al, bh);
        left = itmp.left;
        itmp = multiply(ah, bl);
        right = itmp.right;
        return *this;
    }
    // Otherwise, we have a mixed interval. Make all 4 combinations
    itmp = multiply(al, bl);
    left = itmp.left;
    right = itmp.right;
    itmp = multiply(al, bh);
    left = min(left, itmp.left);
    right = max(right, itmp.right);
    itmp = multiply(ah, bl);
    left = min(left, itmp.left);
    right = max(right, itmp.right);
    itmp = multiply(ah, bh);
    left = min(left, itmp.left);
    right = max(right, itmp.right);

    return *this;
}

Interval Division

The Interval division is:

Image 13

C++
// Works for all classes
// [EMPTY]:=a/[EMPTY] or [EMPTY]:=[EMPTY]/b or [EMPTY]:=[EMPTY]/[EMPTY]
// Please note that this is for all integer classes. interval<int>,interval<long>
// where there is no loss of precision
template<class _IT> inline interval<_IT>& interval<_IT>::operator/=(const interval<_IT>& b)
 {
     const _IT infi(INFINITY);
     // Handle EMPTY interval first
     if (type == EMPTY)
         return *this;
     if (b.type == EMPTY)
         return (*this = b);

     interval<_IT> tmp = *this;  // Save a copy
     // Compute the reverse of y e.g. 1/y
     auto inverse = [&](const _IT& y, const bool up)
     {
         _IT res = _IT(1) / y, r;

         r = -fma(res, y, _IT(-1));
         if (up == false)
         {
             if (r < _IT(0))
                 res = nextafter(res, -infi);
         }
         else
         {
             if (r > _IT(0))
                 res = nextafter(res, +infi);
         }

         return res;
     };

     _IT bl = b.inf();
     _IT bh = b.sup();
     if (bl == _IT(0) && bh == _IT(0))
     {
         left = -infi;
         right = +infi;
         *this *= tmp;
         return *this;
     }
     if (bh == _IT(0))
     {    // b.low is !=0
         right = inverse(bl, true);
         left = -infi;
         *this *= tmp;
         return *this;
     }
     if (bl == _IT(0))
     {    // b.right is !=0
         left = inverse(bh, false);
         right = +infi;
         *this *= tmp;
         return *this;
     }
     // neither b.low or b.right is zero
     left = inverse(bh, false);
     right = inverse(bl, true);
     *this *= tmp;
     return *this;
 }

Implementation of Monadic and Dyadic Arithmetic Operators

We simply compute the dyadic operators with the use of the essential operators to simplify the code.

C++
// Binary + operator specialization for only interval<_IT> arguments
// Works for all classes
//
template<class _IT> inline interval<_IT> operator+(const interval<_IT>& a, const interval<_IT>& b)
{
       interval<_IT> result(a);
       result += b;
       return result;
}

// Unary + operator
// Works for all classes
//
template<class _IT> inline interval<_IT> operator+(const interval<_IT>& a)
{
       return a;
}

// Binary - operator
// Works for all classes
//
template<class _IT> inline interval<_IT> operator-(const interval<_IT>& a, const interval<_IT>& b)
{
       interval<_IT> result(a);
       result -= b;
       return result;
}

// Unary - operator
// Works for all classes
//
template<class _IT> inline interval<_IT> operator-(const interval<_IT>& a)
{
       interval<_IT> result(0);
       result -= a;
       return result;
}

// Binary * operator
// Works for all classes
//
template<class _IT> inline interval<_IT> operator*(const interval<_IT>& a, const interval<_IT>& b)
{
       interval<_IT> result(a);
       result *= b;
       return result;
}

//Binary / operator
// Works for all classes
//
template<class _IT> inline interval<_IT> operator/(const interval<_IT>& a, const interval<_IT>& b)
{
       interval<_IT> result(a);

       if (result == b)
       {
             enum int_class intclass = b.isClass();
             if (intclass != ZERO && intclass != POSITIVE0 && intclass != NEGATIVE0)
             {
                    result = interval<_IT>(1, 1);
                    return result;
             }
       }
       result /= b;
       return result;
}

Implementation of the Comparison Operators

In this section, we handle all the 6 comparison operators. ==,!=, >=, <=, >, <. Notice that we also need to be able to handle the empty interval ∅. If both intervals are ∅, then we return false. If one of the intervals is ∅, then we return true. Notice that some implementations return undefined when both intervals are ∅.

C++
// == operator
// Works for all classes
//
template<class _IT> inline bool operator==(const interval<_IT>& a, const interval<_IT>& b)
{
       if (a.intervaltype() == EMPTY && b.intervaltype() == EMPTY)
             return false; // Both EMPTY=> return false
       if (a.intervaltype() == EMPTY || b.intervaltype() == EMPTY)
             return true;  // One but not both are EMPTY => return true
       // Check for equality. By using inf() and sup() we do not have to worry
       about Improper interval
       // or interval type != CLOSE
       return a.inf() == b.inf() && a.sup() == b.sup();
}

// != operator
// Works for all classes
//
template<class _IT> inline bool operator!=(const interval<_IT>& a, const interval<_IT>& b)
{
       return !(a == b);
}

// >= operator
// Works for all classes
//
template<class _IT> inline bool operator>=(const interval<_IT>& a, const interval<_IT>& b)
{
       if (a.intervaltype() == EMPTY && b.intervaltype() == EMPTY)
             return false;
       if (a.intervaltype() == EMPTY || b.intervaltype() == EMPTY)
             return true;
       if (a.inf() >= b.sup())
             return true;
       return false;
}

// <= operator
// Works for all classes
//
template<class _IT> inline bool operator<=(const interval<_IT>& a, const interval<_IT>& b)
{
       if (a.intervaltype() == EMPTY && b.intervaltype() == EMPTY)
             return false;
       if (a.intervaltype() == EMPTY || b.intervaltype() == EMPTY)
             return true;
       if (a.sup() <= b.inf())
             return true;
       return false;
}

// > operator
// Works for all classes
//
template<class _IT> inline bool operator>(const interval<_IT>& a, const interval<_IT>& b)
{
       if (a.intervaltype() == EMPTY && b.intervaltype() == EMPTY)
             return false;
       if (a.intervaltype() == EMPTY || b.intervaltype() == EMPTY)
             return true;
       if (a.inf() > b.sup() )
             return true;
       return false;
}

// < operator
// Works for all classes
//
template<class _IT> inline bool operator<(const interval<_IT>& a, const interval<_IT>& b)
{
       if (a.intervaltype() == EMPTY && b.intervaltype() == EMPTY)
             return false;
       if (a.intervaltype() == EMPTY || b.intervaltype() == EMPTY)
             return true;
       if (a.sup() < b.inf())
             return true;
       return false;
}

Implementing the sqr(x) and sqrt(x) Functions

We need to add a special code for handling x2. See previous sections. For the sqrt(x), we again can use the fma() library function to quickly bound the interval.

C++
// sqr(x)=x^2
template<class _IT> inline interval<_IT> sqr(const interval<_IT>& x)
{
    _IT left = x.inf();
    _IT right = x.sup();
    _IT tmpl = left;
    _IT tmpr = right;
    interval<_IT> r(0);

    left *= left;
    right *= right;
    r.rightinterval(max(left, right));
    // Contained zero?
    if ( tmpl > 0 && tmpr > 0)
        r.leftinterval(min(left, right));
    return r;
}

// sqrt(x)
template<class _IT> inline interval<_IT> sqrt(const interval<_IT>& x)
{
    const _IT infi(INFINITY);
    _IT sq, r;
    _IT left, right;

    // Find leftinterval bound
    sq = sqrt(x.inf());
    left = sq;
    r = -fma(sq, sq, -x);
    if (r < _IT(0))
        left = nextafter(left, -infi);

    // Find rightinterval bound
    sq = sqrt(x.sup());
    right = sq;
    r = -fma(sq, sq, -x);
    if (r > _IT(0))
        right = nextafter(right, +infi);
    return interval<_IT>(left, right);
}

Implementing the abs(x) Functions

In interval arithmetic, the absolute value (abs) of an interval is defined in a way that captures all possible absolute values of any number within the interval. The result is always a non-negative interval. The definition depends on the position of the interval concerning zero:

If the interval is entirely non-negative (i.e., both its lower and upper bounds are greater than or equal to zero), then the absolute value of the interval is the interval itself, since all values within it are already non-negative.

If the interval is entirely negative (i.e., both its lower and upper bounds are less than zero), then the absolute value of the interval is obtained by taking the absolute values of its bounds and swapping them (since the lower bound will be the most negative and, thus, have the highest absolute value).

If the interval spans zero (i.e., its lower bound is negative and its upper bound is positive), then the absolute value of the interval is from zero to the maximum of the absolute values of the lower and upper bounds.

Source code for abs(x):

C++
// abs([a,b])
// if a>=0 in [a,b] then |[a,b]|==[a,b]
// if b<0 in [a,b] then |[a,b]|=[-b,-a]
// if a<0 & b>0 in [a,b] then |[a,b]|=[0,max(-a,b)]
template<class _IT> inline interval<_IT> abs( const interval<_IT>& a )
       {
       if (a.inf() >= _IT(0) ) // entirely positive
             return a;
       else
             if (a.sup() < _IT(0) ) // Entirely negative
                    return -a;

       return interval<_IT>(_IT(0), max(-a.inf(),a.sup()));
       }

Implementation of the cin & cout Operators

The standard output cout and input cin are straightforward. cout outputs the interval in the format of:

[ left, right ]

When the interval is closed:

( left, right ] or [ left, right ) for semi-open intervals and

And:

( left, right ) for fully open intervals.

If the interval is empty, an ∅ is outputted.

While cin can handle a little variation to the above. Any syntax for the input is legal.

[ left, right ]
[ interval ] which is equivalent to [ interval, interval ]

you can mix in ‘(‘ or ‘)’ instead of square bracket ‘[‘ or ‘]’ to indicate semi or full-open intervals.

Phase One Conclusion

This was the goal for the first phase which defined and implemented all the base base-level functionality that is needed for the next phases.

However, we do have a shortcoming. Where the result is not completely accurate. This has to do with the constructor. The constructor parameter is of the template type _IT (either float, double, or long double). As per C++ standard rules, when an actual parameter is of a lesser type than _IT, then an automatic conversion is applied to the desired _IT type. However, the conversion happens before executing the constructor and therefore any inaccuracies in the conversion are not caught by the interval class. An example will clarify the issue.

C++
Interval<float> x(16777216);
Interval<float> y(16777217);

Will create the same interval [16777216.0,16777216.0] for both x and y. The issue is that the integer 16777217 is larger than what accurately can be converted to a float 23-bit mantissa while 16777216 just fits in.

The same is true for converting a 64-bit double to a 32-bit float type. Even for an interval<double> variable, a 64-bit integer larger than 9,007,199,254,740,992 will not convert correctly to a double (a double mantissa is 52-bit.

In phase 2, we will introduce mixed data type which magically allows us to address and fix the above issue.

This concludes Phase One of the practical implementation of an interval template library. We now have the basic functionality established and we can continue implementing Phase Two and Phase Three.

To read the entire paper which includes the phase two and phase three implementation, the full source and document can be found on my web site at Interval Arithmetic (hvks.com).

References

  1. Intel 64 and IA32 Architectures Software Developers Manual. Volume 2. June 2014
  2. Wilkinson, J H, Rounding errors in Algebraic Processes, Prentice-Hall Inc, Englewood Cliffs, NJ 1963
  3. Richard Brent & Paul Zimmermann, Modern Computer Arithmetic, Version 0.5.9 17 October 2010; http://maths-people.anu.edu.au/~brent/pd/mca-cup-0.5.9.pdf
  4. T. Hickey & M.H. van Emden. Interval Arithmetic: from Principles to Implementation
  5. Hend Dawood, Theories of Interval Arithmetic, Mathematical Foundations and Applications, Lambert Academic Publishing. 2011. ISBN: 978-3-8465-0152-2
  6. Wikipedia, Interval Arithmetic, https://en.wikipedia.org/wiki/Interval_arithmetic,
  7. Daniel Pfeffer, Interval arithmetic using round-to-nearest, https://www.codeproject.com/Articles/1040839/Interval-arithmetic-using-round-to-nearest-mode-pa
  8. N.T. Hayes, February 2013, Standard for Modal Interval Arithmetic.
  9. J.G. Role, Interval Arithmetic and Analysis: An Introduction

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Denmark Denmark
With a Master's degree in Electronic Engineering, I've further specialized in the intersection of Computer Science, Numerical Analysis, and Applied Mathematics. In my earlier career, I gained extensive experience in developing compilers, linkers, and interpreters. This foundational work has equipped me to later focus on designing high-performance algorithms for arbitrary precision arithmetic. My expertise lies in leveraging optimization techniques to solve complex problems within these domains.

Comments and Discussions

 
QuestionPhase 3: implementation of trigonometric functions (the code on your site) Pin
Daniel Pfeffer27-Mar-24 11:48
professionalDaniel Pfeffer27-Mar-24 11:48 
AnswerRe: Phase 3: implementation of trigonometric functions (the code on your site) Pin
Henrik Vestermark30-Mar-24 0:55
Henrik Vestermark30-Mar-24 0:55 
QuestionVery good article, but with two minor issues Pin
Daniel Pfeffer27-Mar-24 11:31
professionalDaniel Pfeffer27-Mar-24 11:31 
AnswerRe: Very good article, but with two minor issues Pin
Henrik Vestermark28-Mar-24 9:26
Henrik Vestermark28-Mar-24 9:26 
GeneralMy vote of 5 Pin
Ștefan-Mihai MOGA27-Mar-24 8:23
professionalȘtefan-Mihai MOGA27-Mar-24 8:23 
PraiseVery interesting and instructive Pin
Member 1334719418-Mar-24 1:48
Member 1334719418-Mar-24 1:48 
GeneralMy vote of 5 Pin
jmaida16-Mar-24 17:05
jmaida16-Mar-24 17:05 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.