Generic vector math for C#


Keywords
generic, numbers, vector, algorithm, number, vectors
License
Apache-2.0
Install
Install-Package Epic.Vectors -Version 1.0.0-pre3

Documentation

Epic.Vectors

Epic.Vectors is a generic C# vector library.

The problem

So what problem is Epic.Vectors trying to solve?

There are already C# libraries for vectors, like the OpenTK one. The problem with these libraries is that they are tied to a specific library. This library is deliberately library agnostic; the developer has no ties with any major code that uses Epic.Vectors.

Thought process

This section describes the rationale for developing Epic.Vectors and how its design patterns were chosen.

Okay, so let's say we want to write a C# program that does a lot of vector math.

Step 1: arrays

Initially, we try to write it with arrays of integers:

// Array initialize syntax makes our code look awesome: 
int[] a = {0, 1, 0};
int[] b = {1, 1};
// However, we can't do a+b; instead we have to do this:
int[] c = {a.X() + b.X(),
           a.Y() + b.Y(),
           a.Z() + 0};

Pros: 1. We get to use the array initializer syntax.

Cons: 1. We can't have vector operators.

Step 2: array wrappers

Okay so now we decide to use a wrapper class for vectors.

public class Vector<T>
{
    public T[] Values { get; private set; }

    public Vector(T[] values)
    {
        Values = values;
    }

    public static Vector<T> operator +(Vector<T> v1, Vector<T> v2)
    {
        throw new NotImplementedException();
    }

    public override string ToString()
    {
        return string.Join(", ", Values);
    }
}

public static class Extensions
{
    public static Vector<T> ToVector<T>(this T[] array)
    {
        return new Vector<T>(array);
    }
}

So now we can do this:

var a = new [] {0, 1, 2}.ToVector();
var b = new [] {9, 9, 9}.ToVector();
var c = a+b;
Console.WriteLine(c); // Output: 9, 10, 11

Pros:

  1. We can still use the nice array initializer syntax, although there's a little boilerplate around it.
  2. We can implement the + operator.
  3. We now have generics
  4. We have a ToString method

Features

Lots of ways to create vectors

// You can use the friendly array initializer syntax with an extension method:     
var aVector = new[]{1,2,3}.ToVector();

{
    // You can use a generic method that doesn't require you to explicitly specify the type:
    var v1 = Vector.Create(6, 7, 4);
    // Note: if you were required to specify the type,
    // the code would look like this:
    //var v1 = Vector.Create<int>(6, 7, 4);

    // You can create vectors that require a certain number of components:
    var v2 = Vector3.Create(0, 0, 1);

    // You can use the constructor:
    var v3 = new Vector3<int>(0, 0, 1);
}

Combining vectors to create more vectors

// Combining vectors is pretty easy:
{
    var a = Vector3.Create(0, 1, 2);
    var b = Vector4.Create(-1, a);
    var a = Vector4.Create(a.Xy, b.Zw);
    Console.WriteLine(b);
    Console.ReadKey();
}

// You can also use extension methods to combine vectors:
{
    var a = Vector3.Create(0, 2, 3);
    var b = a.ToVector4(6); // b = 0, 2, 3, 6
    var c = b.Yx.ToVector3(9); // c = 2, 0, 9 
}

Implements IEnumerable

// Implements IEnumerable:
foreach(var element in aVector)
{
    Console.WriteLine(element);
}

Implements IReadOnlyList

// Implements IReadOnlyList:
for(var i = 0; i < aVector.Count; i++)
{
    Console.WriteLine(aVector[i]);
}

Supports operators like *, +, /, -

// Supports operators:
{
    var a = new[] { 0, 1, 2 }.ToVector();
    var b = new[] { 0, 1, 2 }.ToVector();
    var c = a*b;
    Console.WriteLine(c);
}

Vector types can specify how many elements

// Includes types that force a specific number of elements in a vector:
{
    var a = new[] {0, 1, 2}.ToVector3();  

    // The following line compiles, but crashes
    // at runtime because only two coordinates
    // were passed in
    var b = new[] {0, 1}.ToVector3();

    var c = new[] {0, 1}.ToVector2();  

    var d = new[] {0, 1, 9, 10}.ToVector4();  
}

X, Y, Z, and W access

// Supports X, Y, Z, and W access:
{
    var ex1 = aVector.X + aVector.Y;
}

// Supports combinations of X, Y, Z, and W access, in any order:
{
    var ex2 = aVector.Xyz; 
    var ex3 = aVector.Xy; 
    var ex3 = aVector.Zyx; 
    var ex3 = aVector.Zywx;
} 

Reactive Extensions

// Supports any operations supported by Epic.Numbers.
// Here, we're adding two event streams of integers.
{
    Numbers.Arithmetic.Plus.PlusUtil<IObservable<int>, IObservable<int>, IObservable<int>>.Plus = Add;

    var a = Vectors.Create(new CustomSubject<int>(0), new CustomSubject<int>(3));
    var b = Vectors.Create(new CustomSubject<int>(0), new CustomSubject<int>(8));

    var aObs = a.Select(subj => subj.AsObservable()).ToVector();
    var bObs = b.Select(subj => subj.AsObservable()).ToVector();

    var c = aObs + bObs;

    c.X.Subscribe(Console.WriteLine);

    a.X.OnNext(9);
    b.X.OnNext(7);

    Console.ReadKey();
}

private static IObservable<int> Add(IObservable<int> arg1, IObservable<int> arg2)
{
    return arg1.CombineLatest(arg2, (a, b) => a + b);
}

// Here, we're adding two vectors, where each element
// of each vector is an event stream of integers.
{
    Numbers.Arithmetic.Plus.PlusUtil<IObservable<Vector3<int>>, IObservable<Vector3<int>>, IObservable<Vector3<int>>>.Plus = Add;

    var a = Vectors.Create(new CustomSubject<Vector3<int>>(Vector3.Default<int>()), new CustomSubject<Vector3<int>>(Vector3.Create(4, 4, 4)));
    var b = Vectors.Create(new CustomSubject<Vector3<int>>(Vector3.Create(0, 0, 0)), new CustomSubject<Vector3<int>>(Vector3.Create(8, 8, 8)));

    var aObs = a.Select(subj => subj.AsObservable()).ToVector();
    var bObs = b.Select(subj => subj.AsObservable()).ToVector();

    var c = aObs + bObs;

    c.X.Subscribe(Console.WriteLine);

    a.X.OnNext(Vector3.Create(0, 3, 2));
    b.X.OnNext(Vector3.Create(0, 3, 2));

    Console.ReadKey();
}

private static IObservable<Vector3<int>> Add(IObservable<Vector3<int>> arg1, IObservable<Vector3<int>> arg2)
{
    return arg1.CombineLatest(arg2, (a, b) => a + b);
}

Limitations

No interface types

There are no interface types that represent vectors. This is required because of this error in C#.

Vectors with different dimensions

You can't add a Vector2<T> and a Vector3<T>; you have to convert one of them to the other type first (e.g., add a zero at the end of the 2D vector). This applies to any differing vector types (Vector<T>, Vector4<T>, Vector3<T>, or Vector2<T>). Example:

var v1 = Vector2.Create(1, 1);
var v2 = Vector3.Create(2, 2, 2);
var v3 = v1 + v2; // this causes a compile-time error.

To add a zero at the end of v1, you could do this:

var v1 = Vector2.Create(1, 1);
var v1_3 = v1.ToVector3(0);
var v2 = Vector3.Create(2, 2, 2);
var v3 = v1 + v2; // this causes a compile-time error.

You can, however, add a Vector<T> and a Vector<T> of course; and Vector<T> objects can have arbitrary dimensions. Like this:

var v1 = Vector.Create(1, 1);
var v2 = Vector.Create(2, 2, 2);
var v3 = v1 + v2; // this compiles and results in v3 = 3, 3, 2

Vectors with different types for each component

You can't have a Vector where the X value is a double and the Y value is an int for example.

Interacting between two vectors with different generic types

You can't use operators on two vectors with differing generic type parameters:

var doubles = Vector.Create(1.0, 2.0);
var ints = Vector.Create(1, 2);
var result = doubles + ints; // this causes a compile-time error

Instead, you should convert them to be the same type, like this:

var doubles = Vector.Create(1.0, 2.0);
var ints = Vector.Create(1, 2);
var result = doubles + ints.Cast<double>().ToVector(); // this does NOT cause a compile-time error

Does not support vectors with unknown sizes

Because the Vector classes implement IReadOnlyList, it is impossible for them to have an unknown size. This means that vectors of infinite size are not supported, as would be possible if the Vector class used IEnumerable only.