I have written a simple Integrator
class in C++17 that can perform either a definite single integration of a single variable or a definite double integration of a single variable using the same integrand.
Here's my Integrator class:
Integrator.h
#pragma once
#include <algorithm>
#include <utility>
#include <functional>
struct Limits {
double lower;
double upper;
Limits(double a = 0, double b = 0) : lower{ a }, upper{ b } {
if (a > b) std::swap(lower, upper);
}
void applyLimits(double a, double b) {
lower = a;
upper = b;
if (a > b) std::swap(lower, upper);
}
};
class Integrator {
private:
Limits limits_;
std::function<double(double)> integrand_;
double dx_;
double dy_;
double integral_;
int step_size_;
public:
Integrator(Limits limits, int stepSize, std::function<double(double)> integrand)
: limits_{ limits },
step_size_{ stepSize },
integrand_{ integrand },
dx_{ 0 }, dy_{ 0 }
{}
~Integrator() = default;
constexpr double dx() const { return this->dx_; }
constexpr double dy() const { return this->dy_; }
constexpr double integral() const { return this->integral_; }
Limits limits() const { return limits_; }
std::function<double(double)>* integrand() { return &this->integrand_; }
// This is always a 1st order of integration!
constexpr double evaluate() {
double distance = limits_.upper - limits_.lower; // Distance is defined as X0 to XN. (upperLimit - lowerLimit)
dx_ = distance / step_size_; // Calculate the amount of iterations by dividing
// the x-distance by the dx stepsize
integral_ = 0; // Initialize area to zero
for (auto i = 0; i < step_size_; i++) { // For each dx step or iteration calculate the area at Xi
dy_ = integrand_(limits_.lower + i * dx_);
double area = dy_ * dx_; // Where the width along x is defines as dxStepSize*i
integral_ += area; // and height(dy) is f(x) at Xi. Sum all of the results
}
return integral_;
}
// This will perform a second order of integration where the inner limits are defined
// by [lower, y] where "upper" is not used directly. This may be expanded in the future...
double integrate(double lower = 0.0, double upper = 0.0) {
// Since we are not using the inner upper limit directly
// make sure that it is still greater than the lower limit
if (upper <= lower) {
upper = lower + 1;
}
// As the code currently stands this temporary is not necessary as I could have
// used the values from the arguments directly, but I wanted to keep it
// for consistency reasons as this might be expanded in the future where the use
// of the upper bound inner limit will be taken into context.
Limits limits(lower, upper);
double outerSum = 0;
dy_ = static_cast<double>(limits_.upper - limits_.lower) / step_size_;
for (int i = 0; i < step_size_; i++) {
double yi = limits_.lower+i*dy_;
double dx_ = static_cast<double>(yi - limits.lower) / step_size_;
double innerSum = 0;
for (int j = 0; j < step_size_; j++) {
double xi = limits.lower + dx_ * j;
double fx = integrand_(xi);
double innerArea = fx*dx_;
innerSum += innerArea;
}
double outerArea = innerSum * dy_;
outerSum += outerArea;
}
integral_ = outerSum;
return integral_;
}
};
This is my driver application:
main.cpp
#include <iostream>
#include <exception>
#include <cmath>
#include "Integrator.h"
constexpr double PI = 3.14159265358979;
constexpr double funcA(double x) {
return x;
}
constexpr double funcB(double x) {
return (x*x);
}
constexpr double funcC(double x) {
return ((0.5*(x*x)) + (3*x) - (1/x));
}
double funcD(double x) {
return sin(x);
}
int main() {
//using namespace util;
try {
std::cout << "Integration of f(x) = x from a=3.0 to b=5.0\nwith an expected output of 8\n";
Integrator integratorA(Limits(3.0, 5.0), 10000, &funcA);
std::cout << integratorA.evaluate() << '\n';
std::cout << "\n\nIntegration of f(x) = x^2 from a=2.0 to b=20.0\nwith an expected output of 2664\n";
Integrator integratorB(Limits(2.0, 20.0), 10000, &funcB);
std::cout << integratorB.evaluate() << '\n';
std::cout << "\n\nIntegration of f(x) = (1\\2)x^2 + 3x - (1\\x) from a=1.0 to b=10.0\nwith an expected output of 312.6974\n";
Integrator integratorC(Limits(1.0, 10.0), 10000, &funcC);
std::cout << integratorC.evaluate() << '\n';
std::cout << "\n\nIntegration of f(x) = sin(x) from a=0.0 to b=" <<PI<< "\nwith an expected output of 2\n";
Integrator integratorD(Limits(0.0, PI), 10000, &funcD);
std::cout << integratorD.evaluate() << '\n';
std::cout << "\n\nTesting Double Integration of f(x) = (1\\2)x^2 + 3x - (1\\x) from [3,5] and [1,y]\nwith an expected output of 65.582\n";
Integrator integratorE(Limits(3, 5), 500, &funcC);
//double dy = integratorE.limits().upper - integratorE.limits().lower;
integratorE.integrate(1);
std::cout << integratorE.integral() << '\n';
} catch (const std::exception& e) {
std::cerr << e.what() << std::endl;
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
And this is my output to the console when I run the program:
Integration of f(x) = x from a=3.0 to b=5.0
with an expected output of 8
7.9998
Integration of f(x) = x^2 from a=2.0 to b=20.0
with an expected output of 2664
2663.64
Integration of f(x) = (1\2)x^2 + 3x - (1\x) from a=1.0 to b=10.0
with an expected output of 312.6974
312.663
Integration of f(x) = sin(x) from a=0.0 to b=3.14159
with an expected output of 2
2
Testing Double Integration of f(x) = (1\2)x^2 + 3x - (1\x) from [3,5] and [1,y]
with an expected output of 65.582
65.3933
Here are my questions and concerns about the above code:
- What kind of improvements can be made to this code? I'm referring to "readability", "generically", and "portability".
- I know that this isn't within a
namespace
as that is not primary concern within the context of this question. I can always put this in some definednamespace
!
- I know that this isn't within a
- Are there any apparent "code smells"?
- I have comments in my
integrate
function on not using the inner upper bounds...- How would I be able to incorporate the use of a defined inner upper bound?
- How can I extend my
integrate
function to perform even high orders of integration? - Considering that the current implementation of performing a double integration has an
O(n^2)
complexity is there a way to reduce to thisO(n)
orO(log N)
? If so, how? - Are there any other optimizations that can be incorporated?
- Would the use of
threads
,multithreading
, andparallel-programming
be applicable here? - Should I template this class?
- I'm also interested in any and all suggestions, tips, and feedback!
Extra useful information in regards to the design and implementation of my class
Its user-defined constructor requires three parameters/arguments in order to create an instance of an Integrator
object.
- Its first requirement is the limits of integration that is defined by a simple
Limits
struct. - Its second requirement is the
step_size
, normally the width ofdx
or the number of divisions in calculating the area of integration by parts. - The third and final requirement is an
std::function<double(double)>
object.
About the Limits
struct:
- It contains the
lower
andupper
bounds of integration froma
tob
. - It has a basic user-defined default constructor that takes the
lower
andupper
bounds of integration as arguments. Both arguments can default to 0. - It also contains an
applyLimits(lower,upper)
function. This simply acts as its constructor does with respect to its members by setting them or updating them. - Access is purely public as these limits can be changed by the user at any given time. There is no restriction on the changing of the limits of integration.
- Both its constructor and its
applyLimits()
function will check iflower
is greater thanupper
and if so it will swap them.
About the function-objects
:
- They can be any of the following:
- function object
- function pointer
- functor
- lambda expression.
- These function objects can be defined as either
constexpr
ornon-constexpr
. - Any are valid as long as they have the signature
double(double)
and can be stored in anstd::function<>
object.
About the construction and the use of the Integrator
class object:
- What it can do
- It can perform a definite integral of a single variable through the use of its
evaluate()
function. - It can also perform a second integral of the same integrand of a single variable through its function
integrate(lower, upper)
. - It can also give you both the current
dy
anddx
values, theintegrand
, and the currentintegral
as well as thelimits
of integration.
- It can perform a definite integral of a single variable through the use of its
- Construction
- The limits or outer limits are defined when the object is instantiated through its user-defined constructor.
- This is the default behavior for both single and double integrations.
- The higher the
step_size
the more accurate the approximation.- Trade-offs: accuracy versus decrease in performance, time of execution taken.
- The function object is stored as its
integrand
. - Versatility in being able to retrieve it back from the Integrator object and being able to use it at any time.
- The inner limits are defined when calling its
integrate()
function.- The inner limits of integration are from
[lower,y]
. lower
is passed into the function as an argument andy
is calculated on each iteration.- Currently within this context,
upper
is ignored for the inner limits and will default to1>lower
so that theLimits
struct doesn't swap them.
- The inner limits of integration are from
- See the note below in regards to expanding this class...
- The limits or outer limits are defined when the object is instantiated through its user-defined constructor.
I'm considering expanding this to also allow for the user input of the inner upper limit of integration to be defined by the user and apply it within the algorithm to generate the appropriate values of integration. This has yet to be implemented and is one of my concerns. I would also like to incorporate an easy way to perform triple, quad, and quint integrations if performance bottlenecks can be reduced to a minimum while still being able to give an accurate approximation without a major decrease in performance. I would like to have the capabilities of an Integrator object to possibly accept another Integrator object as an argument.