morph::vvec (dynamically resizable mathematical vector)
#include <morph/vvec.h>
Header file: morph/vvec.h. Test and example code: tests/testvvec
Table of contents
- morph::vvec (dynamically resizable mathematical vector)
- Summary
- Design
- Arithmetic operators
- Assignment operators
- Comparison operators
- Member functions
Summary
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;
Template arguments are S
, the element type and N
, the size of the array.
Create objects just like std::vector
:
morph::vvec<int> v1 = { 1, 2, 3, 4 };
but with morph::vvec
, and just like morph::vec
, you can do maths:
morph::vvec<int> v2 = { 1, 2, 3, 4 };
morph::vvec<int> v3 = v1 + v2; // element-wise addition
morph::vvec<float> u1 = { 1.0f, 0.0f, 0.0f };
morph::vvec<float> u2 = { 0.0f, 1.0f, 0.0f };
morph::vvec<float> u3 = u1.cross (u2); // vector cross-product
float dp = u1.dot (u3); // (scalar/dot/inner)-product
Design
Like morph::vec
, vvec
derives from an STL container without apology to give us a dynamically resizable container of scalars or vectors on which mathematical operations can be called.
vvec
is very similar to vec
, sharing many member functions with the same name.
Arithmetic operators
You can use arithmetic operators on vvec
objects with their operations being applied element wise. operations with other vvec
objects and with scalars are all supported and should work as expected.
Operator | Scalar example | Element wise vvec example |
---|---|---|
+ | v2 = v1 + 2.0f; or v2 = 2.0f + v1; | v3 = v1 + v2; |
+= | v2 += 2.0f; | v2 += v1; |
- | v2 = v1 - 5.0f; or v2 = 5.0f - v1; | v3 = v1 - v2; |
-= | v2 -= 2.0f; | v2 -= v1; |
* | v2 = v1 * 10.0; or v2 = 10.0 * v1; | v3 = v1 * v2; |
*= | v2 *= 10; | v2 *= v1; |
/ | v2 = v1 / 10.0; or v2 = 10.0 / v1; | v3 = v1 / v2; |
/= | v2 /= 10; | v2 /= v1; |
- (unary negate) | v2 = -v1; |
Assignment operators
The assignment operator =
will work correctly to assign one vvec
to another. For example,
morph::vvec<float> v1 = { 1, 2, 3 };
morph::vvec<float> v2 = v1; // Good, works fine
Because std::vector
is the base class of vvec
it is also possible to assign a vvec
to an std::vector
:
morph::vvec<float> v1 = { 1, 2, 3 };
std::vector<float> sv1 = v1; // Good, works fine
However, there are no templated assignment operators in morph::vvec
that make it possible to assign from other types. For example, this will not compile:
std::vector<float> sv1 = { 1, 2, 3 };
morph::vvec<float> v1 = sv1; // Bad, doesn't compile
Comparison operators
The default comparison in std::vector
(and also in std::array
) is a lexicographic comparison. This means that if the first element in a first vector
is less than the first element in a second vector
, then the first vector
is less than the second vector
regardless of the values in the remaining elements. This is not useful when the vector is interpreted as a mathematical vector. In this case, an appropriate comparison is probably vector magnitude comparison (i.e comparing their lengths). Another useful comparison is elementwise comparison where one vector may be considered to be less than another if each of its elements is less than the corresonding other element. That is to say v1
< v2
if v1[i]
< v2[i]
for all i
. Both vector magnitude and elementwise comparison can be applied to a morph::vvec
and a scalar.
In morph::vvec
I’ve implemented the following comparisons. Note that the default is not lexicographic comparison and that some comparisions could still be implemented.
Comparison (vvec v1 with vvec v2 ) | Lexicographic | Vector magnitude | Elementwise |
---|---|---|---|
v1 < v2 | v1.lexical_lessthan (v2) | v1.length_lessthan (v2) | v1 < v2 |
v1 <= v2 | not implemented | v1.length_lte (v2) | v1 <= v2 |
v1 > v2 | not implemented | v1.length_gtrthan (v2) | v1 > v2 |
v1 >= v2 | not implemented | v1.length_gte (v2) | v1 >= v2 |
Comparison (vvec v with scalar s ) | Lexicographic | Vector magnitude | Elementwise |
---|---|---|---|
v < s | not implemented | not implemented | v < s |
v <= s | not implemented | not implemented | v <= s |
v > s | not implemented | not implemented | v > s |
v >= s | not implemented | not implemented | v >= s |
‘not’ comparison | Implementation |
---|---|
unary not operator ! | operator! implements length comparison. !v1 is true if v1.length() == 0 |
not operator != | unimplemented |
Using morph::vvec
as a key in std::map
or within an std::set
Although morph::vvec
derives from std::vector
, you can’t use it as a key in an std::map
!
Danger! The following examples will compile but will have unexpected runtime results:
// Bad!! Because the key is morph::vvec<int>
std::map<morph::vvec<int>, myclass> some_map;
// Also Bad! Again, the key is morph::vvec<int>
std::set<morph::vvec<int>> some_set;
The reason for this is that std::set
and std::map
depend upon the less-than comparison for their functionality and the default element-wise less-than in morph::vvec
is elementwise which will fail to sort an array. In std::vector
, less-than is lexicographic which guarantees a successful sort.
The workaround is to specify that you want to use the morph::vvec
lexicographical less-than function lexical_lessthan
in your map
or set
:
// To make the map work, we have to tell it to use lexical_lessthan:
auto _cmp = [](morph::vvec<int> a, morph::vvec<int> b){return a.lexical_lessthan(b);};
std::map<morph::vvec<int>, std::string, decltype(_cmp)> themap(_cmp);
This example is adapted from tests/testvec_asmapkey and may need to be tested!
Member functions
Setter functions
Rather than using implementations of the assignment operators (I did try, but it turned out to be too difficult), vvec
provides set_from
functions. There is an overload of set_from
which can assign values to your vvec
from another container of values and an overload which will assign a single value to every element of your vvec
.
Note that the set_from
functions in vvec
differ from those in morph::vec
.
Setting a vvec from another container of values
If you want to set your vvec values from another container, such as a std::vector
, then you can use this templated overload:
template <typename Container>
std::enable_if_t<morph::is_copyable_container<Container>::value, void>
void set_from (const Container& c)
{
this->resize (c.size());
std::copy (c.begin(), c.end(), this->begin());
}
As long as you pass in a ‘copyable container’ (which means a vector
, array
, deque
, vvec
, vec
or set
, but not a map
) then this will resize your vvec
to match the passed in container and then std::copy the values into your vvec
.
Setting all the elements of a vvec to the same value
All of these functions set all the elements of your vvec
to a single value.
void set_from (const _S& v);
void zero();
void set_max();
void set_lowest();
The set_from
overload fills all elements of the morph::vvec
with v
.
zero()
, set_max()
and set_lowest()
fill all elements with S{0}
, the maximum possible value for the type and the lowest possible value, respectively.
Using the parent class’s assign
method
You can, of course, also use the assign()
method from std::vector
:
morph::vvec<int> vv = { 0, 0, 0, 0, 0}; // construct with 5 elements
vv.assign (3, 10); // resize vv to 3 elements and assign 10 to each element
std::cout << vv << std::endl; // output: (10,10,10)
or
std::vector<int> vin = { 1, 2, 3 };
morph::vvec<int> vv (vin.size());
vv.assign (vin.begin(), vin.end());
std::cout << vv << std::endl; // output: (1,2,3)
Numpy clones
void linspace (const _S start, const _S2 stop, const size_t num=0);
void arange (const _S start, const _S2 stop, const _S2 increment);
Python Numpy-like functions to fill the morph::vvec
with sequences of numbers. linspace
fills the vvec
with num
values in a sequence from start
to stop
. arange
resizes the vvec
and fills it with elements starting with start
and ending with stop
incrementing by increment
.
Random numbers
Three functions to fill a vvec
with random numbers:
void randomize(); // fill from uniform random number generator, range [0,1).
void randomize (S min, S max) // fill from uniform RNG, range [min,max).
void randomizeN (S _mean, S _sd) // fill from normal RNG with given mean and std. deviation.
Re-ordering elements
There’s a shuffle function:
void shuffle(); // Randomize the order of the existing elements
vvec<S> shuffled(); // Return a vvec copy with randomized the order of the existing elements
And rotates:
void rotate(); // Rotate '1 step to the left'
template <typename T=int> void rotate (T n); // Rotates 'n steps to the left'
void rotate_pairs(); // If size is even, permute pairs of elements in a rotation. 0->1, 1->0, 2->3, 3->2, etc.
Type conversions
These return a new vvec
in the requested type:
vvec<float> as_float() const;
vvec<double> as_double() const;
vvec<int> as_int() const;
vvec<unsigned int> as_uint() const;
For example:
morph::vvec<int> vi = {1,2,3};
morph::vvec<float> vf = vi.as_float(); // Note: new memory is used for the new object
String output
std::string str() const;
std::string str_mat() const;
std::string str_numpy() const;
These functions output the vvec
as a string in different formats. The _mat and _numpy versions generate text that can be pasted into a session of MATLAB/Octave or Python. Output looks like (1,2,3)
(str()
), [1,2,3]
(str_mat()
) or np.array((1,2,3))
(str_numpy()
). If you stream a vvec
then str()
is used:
morph::vvec<int> v = {1,2,3}; // Make a vvec called v
std::cout << v; // Stream to stdout
gives output (1,2,3)
.
Length, lengthen, shorten
template <typename _S=S>
_S length() const; // return the vector length
_S length_sq() const; // return the vector length squared
_S sos() const; // also length squared. See header for difference
// Enabled only for non-integral S:
vvec<S> shorten (const S dl) const; // return a vector shortened by length dl
vvec<S> lengthen (const S dl) const; // return a vector lengthened by length dl
The range and rescaling or renormalizing
You can obtain the range of values in the vvec
with vvec::range
which returns a morph::range object:
morph::range<S> range() const;
Example usage:
morph::vvec<float> v = { 1, 2, 3 };
morph::range<float> r = v.range();
std::cout << "vvec max: " << r.max << " and min: " << r.min << std::endl;
To re-scale or renormalize the values in the vvec
:
void renormalize(); // make vector length 1
void rescale(); // rescale to range [0,1]
void rescale_neg(); // rescale to range [-1,0]
void rescale_sym(); // rescale to range [-1,1]
Renormalization takes a vector and makes it have length 1. Thus, renormalize()
computes the length of the vvec
and divides each element by this length.
Use rescale
when you want the values in the array to range between 0 and 1. recale
will find a linear scaling so that the values in the vec
will be in the range [0,1]. rescale_neg
finds a scaling that puts the values in the range [-1,0]. rescale_sym
puts the values in the range [-1,1].
Note that these functions use a template to ensure they are only enabled for non-integral types:
template <typename _S=S, std::enable_if_t<!std::is_integral<std::decay_t<_S>>::value, int> = 0 >
Check whether your renormalized vector is a unit vector:
bool checkunit() const; // return true if length is 1 (to within vvec::unitThresh = 0.001)
Finding elements
A group of functions to find the value or index of particular elements in the array.
S longest() const; // return longest element (greatest magnitude)
size_t arglongest() const; // return index of longest element
S shortest() const; // return shortest element (closest to 0)
size_t argshortest() const; // return index of shortest element
S max() const; // return max element
size_t argmax() const; // return index of max element
S min() const; // return min element
size_t argmin() const; // return index of min element
Content tests (zero, inf, NaN)
bool has_zero() const;
bool has_inf() const; // can only return true if type S has infinity
bool has_nan() const; // can only return true if type S has NaN
bool has_nan_or_inf() const; // can only return true if type S has infinity and/or NaN
Replacing NaN and inf values
To replace all not-a-numbers in a vvec
with another value use:
void replace_nan_with (const S replacement);
To replace NaNs and infinitities, it’s:
void replace_nan_or_inf_with (const S replacement);
Search/replace and thresholding
vvec
has search/replace:
void search_replace (const S searchee, const S replacement);
And threshold functions which replace any values outside limits with the limit.
vvec<S> threshold (const S lower, const S upper) const;
void threshold_inplace (const S lower, const S upper);
Pruning - removing elements from a vvec
There are also ‘pruning’ functions to remove positive or negative or nan values in a vvec
, thus changing the size of the vvec or returning a vvec of smaller size.
vvec<S> prune_positive() const;
void prune_positive_inplace();
vvec<S> prune_negative() const;
void prune_negative_inplace();
vvec<S> prune_nan() const;
void prune_nan_inplace();
Simple statistics
// These template functions are declared with a type _S:
template<typename _S=S>
_S mean() const; // The arithmetic mean
_S variance() const; // The variance
_S std() const; // The standard deviation
_S sum() const; // The sum of all elements
_S product() const; // The product of the elements
Maths functions
Raising elements to a power.
vvec<S> pow (const S& p) const; // raise all elements to the power p, returning result in new vec
void pow_inplace (const S& p); // in-place version which operates on the existing data in *this
template<typename _S=S>
vec<S, N> pow (const vvec<_S>& p) const; // Raise each element in *this to the power of the matching element in p (which must be same size)
template<typename _S=S>
void pow_inplace (const vvec<_S>& p); // in-place version
The signum function is 1 if a value is >0; -1 if the value is <0 and 0 if the value is 0.
vvec<S> signum() const; // Return the result of the signum function in a new vec
void signum_inplace(); // in-place version
Floor computes the largest integer value not greater than an element value. This is applied to each element.
vvec<S> floor() const; // Return the result of the floor function in a new vec
void floor_inplace(); // in-place version
Ceil computes the least integer value not less than an element value. This is applied to each element.
vvec<S> ceil() const; // Return the result of the ceil function in a new vec
void ceil_inplace(); // in-place version
Trunc computes the nearest integer not greater in magnitude than element value. This is applied to each element.
vvec<S> trunc() const; // Return the result of the trunc function in a new vec
void trunc_inplace(); // in-place version
Square root or the square. These are convenience functions. For cube root, etc, use pow()
.
vvec<S> sqrt() const; // the square root
void sqrt_inplace();
vvec<S> sq() const; // the square
void sq_inplace();
Logarithms and exponential
vvec<S> log() const; // element-wise natural log
void log_inplace();
vvec<S> log10() const; // log to base 10
void log10_inplace();
vvec<S> exp() const; // element-wise exp
void exp_inplace();
absolute value/magnitude
vvec<S> abs() const; // element-wise abs()
void abs_inplace();
The scalar product (also known as inner product or dot product) can be computed for two vvec
instances, which must have equal size (otherwise a runtime error is thrown).
template<typename _S=S>
S dot (const vvec<_S>& v) const
The cross product is defined here only for vvec
of size 3.
If N
is 2, then v x w is defined to be v_x w_y - v_y w_x and for N=3, see your nearest vector maths textbook. The function signatures are
template<typename _S=S>
S cross (const vvec<_S>& w) const;