Using the variable vector class, morph::vvec

vvec is a dynamically re-sizable array which derives from std::vector. It extends std::vector by providing maths methods. It’s a convenient and simple linear maths library, all in a single header. The elements of a vvec take the template type for the class, which is commonly a scalar such as int, float or double. However, it is also possible to create a vvec of vectors or coordinates.

Here’s how the class is declared:

    template <typename S=float, typename Al=std::allocator<S>>
    struct vvec : public std::vector<S, Al>
    {
        //! We inherit std::vector's constructors like this:
        using std::vector<S, Al>::vector;

Creating a vvec

Easy! Just do it as if it were an std::vector:

morph::vvec<int> vv = { 1, -4, 6, 8 };

That creates and initialises a vvec containing four int numbers. If you want to initialise every element to the same value (such as 0), you can instead (again, just like std::vector):

morph::vvec<int> vv (4, 0);

Note that you can also resize() a vvec and call reserve(), just as for std::vector.

We can place either of the initialisers above into a program, and then send the content of the vvec to stdout with the line:

std::cout << vv << std::endl;

A complete, short program example is:

#include <iostream>
#include <morph/vvec.h>
int main()
{
    morph::vvec<int> vv = { 1, -4, 6, 8 };
    std::cout << vv << std::endl;
    return 0;
}

When you compile and run, you should see this:

~/codeproject$ g++ -I. -o vvec vvec.cpp && ./vvec
(1,-4,6,8)
~/codeproject$

vvec has the ability to be streamed, and places brackets around the comma-separated numbers.

Filling the vvec with Values

You can set the values in your vector with a number of functions. Anything you could do with an std::vector will work, such as:

morph::vvec<float> vf (4); // Create a vvec of size 4
vf[0] = 1.0f; // Set the first element to the value 1
vf[1] = 2.0f;

or:

float f = 0.0f;
for (auto& v : vf) {
    v = f;
    f += 1.0f;
}

There are also a number of set_from functions:

morph::vvec<float> vf;

// set values from an std::array
std::array<float, 5> arr1 = { 5.0f, 4.0f, 3.0f, 2.0f, 1.0f };
vf.set_from (arr1);
std::cout << vf << std::endl;

// set values from a single scalar
vf.set_from (7.5f);
std::cout << vf << std::endl;

which gives this output:

(5,4,3,2,1)
(7.5,7.5,7.5,7.5,7.5)

Note that when setting from a fixed size array, our vvec gets resized, but when set_from a single scalar is called, the vvec’s size is maintained and every element is given the passed in value of 7.5f.

You can set all elements of a vvec to zero or the max/lowest possible value of the element type with these functions:

morph::vvec<unsigned short> vus (10);
vus.zero();       // Set all elements to 0
vus.set_max();    // Set all elements to max for unsigned short (65535)
vus.set_lowest(); // Set all elements to the lowest/most negative possible value (0).

A numpy-like linspace() function for C++

Another way to fill your vvec is to use vvec::linspace, which works like Python numpy’s linspace:

morph::vvec<double> vd;
vd.linspace (0, 1.0, 4);
std::cout << vd << std::endl

Output is:

(0,0.33333333333333331,0.66666666666666663,1)

Randomize the Content of the Vector

To place random values in the vector from a uniform distribution, call vvec::randomize().

morph::vvec<float> vf;
vf.resize(3);
vf.randomize();
std::cout << vf << std::endl;

This sets each element of vf from a uniform random distribution with values between 0 and 1. The program might output:

(0.978181541,0.484534025,0.824066818)

You can also choose the range for the uniform random number generator with a min and max range specifier:

vf.randomize (4.0f, 6.0f);
std::cout << vf << std::endl;

Giving example output:

(5.53486824,5.04247379,4.88938808)

If you want to set values by selecting values from a normal distribution, then use randomizeN():

float mean = 0.0f; // The mean of the normal distribution
float sd = 3.7f;   // The standard deviation of the distribution
vf.randomizeN (mean, sd);

Doing Some Math

Basic Arithmetic Operations

Let’s actually do some maths with those vectors. When you have a one-dimensional array of numbers, you will often want to do something to the numbers element by element. For example, you might want to add corresponding elements in two vectors of the same length together.

Here’s how you can create two vectors and add them together with vvec:

morph::vvec<double> v1 = { 2, 3, 4 };
morph::vvec<double> v2 = { 1, -4, 4.5 };
std::cout << v1 << "+" << v2 << "=" << v1 + v2 << std::endl;

Notice how simple that is? You just write v1 + v2. The operator overload returns a new vvec containing the element-wise sum of v1 and v2. Other operators are defined:

std::cout << v1 << "*" << v2  << "=" << v1 * v2  << std::endl;
std::cout << v1 << "+" << 3.0 << "=" << v1 + 3.0 << std::endl;
std::cout << 1  << "/" << v1  << "=" << 1.0/v1   << std::endl;
std::cout << v2 << "/" << 2.0 << "=" << v2/2.0   << std::endl;

For each operator, you can also make use of the op= version. So these are all valid:

v1 += v2;
v1 -= 4.5;
v1 /= 6.0;
v1 *= v2;

On the whole, if the two operands are vvecs, then the operation is carried out element-wise; if one of the operands is a scalar, then this scalar is added/subtracted/multiplied by each element of the vvec operand. Vector Operations

One intention of morph::vvec (and especially its fixed-size cousin morph::vec) was to do genuine vector algebra - vector addition, subtraction and scaling. These are covered by the basic arithmetic operators described above, but in addition (to addition) the cross product and scalar product operations are defined.

You can compute the scalar product (also known as the inner product or the dot product) of two vectors that have the same number of elements.

double scalar_product = v1.dot (v2);

The cross product is also defined, as long as the vectors have three elements. Mathematicians have defined cross products for other dimensionalities, and if you need one of those cross products, then please feel free to make a pull request on Github with your solution. The plain, 3D cross product can be very useful; here’s an example with unit vectors:

morph::vvec<double> u1 = { 1, 0, 0 };
morph::vvec<double> u2 = { 0, 1, 0 };
std::cout << u1 << " cross " << u2 << " = " << u1.cross (u2) << std::endl;

Functions

Another way to think of the numbers in your vvec are as a series, such as a time series. You can use linspace to define a sequence of times, and then compute a number of different functions as follows:

// Create a vvec of 100 time points from 0 to 10
morph::vvec<float> t;
t.linspace (0, 10, 100);

float s = 1.0f;
float p = 3.0f;
// Example functions that can be applied to t
morph::vvec<float> sine_of_t        = t.sin();     // sine of t
morph::vvec<float> cos_of_t         = t.cos();     // cosine of t
morph::vvec<float> exp_of_t         = t.exp();     // natural exponential (e^t)
morph::vvec<float> log_of_t         = t.log();     // natural log (base e)
morph::vvec<float> log10_of_t       = t.log10();   // log (base 10)
morph::vvec<float> gaussian_of_t    = t.gauss(s);  // Gaussian fn: e^(-t*t/2*s*s)
morph::vvec<float> third_power_of_t = t.pow(p);    // powers (here, t^3)
morph::vvec<float> sqrt_of_t        = t.sqrt();    // square root of t
morph::vvec<float> sq_of_t          = t.sq();      // the square of t

Note that a second vvec is created in memory for the return data, and the values in t are not affected. If, instead, you want to compute the function of t, replacing the values in t, then you can call the corresponding “in place” versions of the functions:

t.sin_inplace();
t.exp_inplace();
// etc

There are a few other operations that can be useful, when applied to a sequence. You may want the absolute value of each element, or to remove elements that are below 0, or you may want the signum of the elements. Here are some examples:

morph::vvec<float> abs_of_t = t.abs();                     // element-wise absolute value
morph::vvec<float> signum_t = t.signum();                  // signum function
morph::vvec<float> zero_and_negative = t.prune_positive(); // prune positive elements
                                                           // (those >0)
morph::vvec<float> zero_and_positive = t.prune_negative(); // prune negative elements
                                                           // (those <0)

Again, these all have _inplace() versions.

Comparisons

One important difference between morph::vvec and std::vector is what happens when you do a comparison such as:

v1 < v2

where both v1 and v2 are arrays of numbers. If v1 and v2 were std::vector objects, then the comparison would, by default, be lexicographic. This will return true if any of the pairs of elements, considered in order, fulfils the comparison. For example:

std::vector<int> v1 = { 10, 50, 2, 3, 700, 90 };
std::vector<int> v2 = { 12, 45, 1, 2, 699, 70 };
std::cout << "Is v1 < v2? " << (v1 < v2 ? "yes, it's less than" : "no,
                                not less than") << std::endl;

This outputs “yes, it’s less than” because 10 < 12, which is the first comparison made. This kind of comparison is good when comparing strings of characters. However, I find it more useful to consider the comparison between arrays to be true only if every element gives a true comparison. For this reason, this example prints “Yes” because 10<12 and 44<45:

morph::vvec<int> vv3 = { 10, 44 };
morph::vvec<int> vv4 = { 12, 45 };
if (vv3 < vv4) { std::cout << "Yes\n"; }
else { std::cout << "No\n"; }

but this example prints “No” because 50 is not less than 45.

morph::vvec<int> vv3 = { 10, 50 };
morph::vvec<int> vv4 = { 12, 45 };
if (vv3 < vv4) { std::cout << "Yes\n"; }
else { std::cout << "No\n"; }

Statistics

It’s straightforward to compute a number of summary statistics from your vector of numbers:

morph::vvec<double> vv = { 0.4, 0.6, -0.8, 0.34 };

std::cout << "The mean of " << vv << " is " << vv.mean() << std::endl;
std::cout << "The sum of " << vv << " is " << vv.sum() << std::endl;
std::cout << "The standard deviation of " << vv << " is " << vv.std() << std::endl;
std::cout << "The cumulative product of " << vv << " is " << vv.product() << std::endl;

Output:

vv = (0.40000000000000002,0.59999999999999998,-0.80000000000000004,0.34000000000000002)
The mean of vv is 0.135
The sum of vv is 0.54
The standard deviation of vv is 0.633167
The cumulative product of vv is -0.06528

Convolutions/Smoothing/Derivatives

Lastly, morph::vvec has a convolve function to convolve a 1D array of numbers with a 1D convolution kernel and a discrete derivative function. Both of these can be applied as if the vvec were wrapped around.

Here’s an example of a convolution:

using mc = morph::mathconst<double>;
using wrapdata = morph::vvec<double>::wrapdata;

// Create x, and initialise with a sequence of values using vvec::linspace
morph::vvec<double> x;
x.linspace (-mc::pi, mc::pi-(mc::pi/5.0), 60);

// Create y as the sine of x
morph::vvec<double> y = x.sin();

// Create some random noise and add to y
morph::vvec<double> r (x.size(), 0.0);
r.randomize();
y += r;

// Manually create a convolution filter
morph::vvec<double> filter = {.2, .4, .6, .8, 1, .8, .6, .4, .2};
filter /= filter.sum();

// convolve y with the filter, copying the result into y2. Apply 1D wrapping.
morph::vvec<double> y2 = y.convolve (filter, wrapdata::wrap);

To see this example graphed, you can check out this example from morphologica: vvec_convolve.cpp

Points of Interest

Because morph::vvec derives from std::vector, it’s possible to pass a morph::vvec to any function that takes a reference to std::vector. This can be useful when working with other libraries.

As far as performance goes, morph::vvec has to be compared to other linear algebra libraries. Because I coded vvec for convenience rather than performance, I didn’t expect it to be anywhere near as fast as other libraries such as Eigen. However, because compiler optimizations are so good, the performance of vvec isn’t bad at all. I haven’t yet had time to make a really good, comprehensive comparison between Eigen, vvec and other linear algebra libraries, but quick tests showed that for some array sizes, vvec was faster than Eigen and for other array sizes, Eigen was the fastest. For complex operations, in which Eigen uses clever templates to combine operations, I’d expect vvec to fall further behind.

However, vvec’s primary selling point is simplicity and convenience. You can code up mathematical operations with ease and the class is a pleasure to use. I hope you enjoy it.