Open krwq opened 4 years ago
Do I understand correctly that you would like to have a Vector3<MagneticField>
or Vector3<Speed>
instance? I guess that will end up with the same problems that were also discussed in #695.
Consider this code:
public class Vector3<T>
{
private T _value;
public Vector3(T value)
{
_value = value;
}
public static Vector3<T> operator+(Vector3<T> a, Vector3<T> b)
{
return new Vector3(a._value + b._value); // ERROR: T does not have an operator +
}
// ....
}
Unfortunatelly the above doesn't work, because the generic type "T" (aka any type) does not have a defined + operator. There's no way to add a type constraint on "something that is a number". And declaring interfaces (IImplementsAddition
) for operator methods is also not possible, because in C# they need to be static by definition 😢 ...
This works:
public static Vector3<T> operator+(Vector3<T> a, Vector3<T> b)
{
dynamic a1 = a;
dynamic b1 = b;
return new Vector3(a1._value + b1._value); // Works!
}
But of course, this comes with a performance penalty (I don't know how big it is) and the downside that problems will pop up only at runtime and not at compile time.
Hi,
UnitsNet does not currently support XYZ-dimensional quantities in a generic way. Some discussion in #695 on why this was not straight forward to add, but it is absolutely possible if someone wants to champion it and a performant proposal is outlined in #666.
You can, however, easily add your own wrapper types for specific quantities. Something like this:
public struct MagneticField3
{
public MagneticField X { get; }
public MagneticField Y { get; }
public MagneticField Z { get; }
public MagneticField3(MagneticField x, MagneticField y, MagneticField z) { ... }
// Operator overloads for arithmetic
public static MagneticField3 operator +(MagneticField3 left, MagneticField3 right) { ... }
public static MagneticField3 operator -(MagneticField3 left, MagneticField3 right) { ... }
public static MagneticField3 operator *(double left, MagneticField3 right) { ... }
public static MagneticField3 operator /(MagneticField3 left, double right) { ... }
Did that answer your question?
@pgrawehr not necessarily, you could alternatively have MagneticField3 struct. Generally speaking the magnetic field definition talks about vectors and not scalars.
I agree it's not ideal that above cannot be expressed too well in C# and generic vector would be a perfect solution.
We could use the code waiting in #698 to do this in a generic way, and without a large performance hit.
Sounds good, but would apparently also need the definition of the Vector
Honestly for units I only care about vector. Matrices are probably only used in more advanced physics (i.e. jacobian) which usually need super high performance in which case you'd likely write your own version anyway. Good idea for completeness but I'd personally skip it for now.
@angularsen that should be fine for now although this is something I'd really love to see here in the future (although understand the limitations and why not yet)
I created #801 as a POC. I assume you would expect to write something as follows:
var length1X = Length.FromMeters(1.0);
var length1Y = Length.FromMeters(2.0);
var length1Z = Length.FromMeters(3.0);
var length2X = Length.FromMeters(4.0);
var length2Y = Length.FromMeters(5.0);
var length2Z = Length.FromMeters(6.0);
var vector1 = new Vector3<Length>(length1X, length1Y, length1Z);
var vector2 = new Vector3<Length>(length2X, length2Y, length2Z);
var result = vector1 + vector2;
var expectedX = Length.FromMeters(5.0);
var expectedY = Length.FromMeters(7.0);
var expectedZ = Length.FromMeters(9.0);
Assert.Equal( new Vector3<Length>( expectedX, expectedY, expectedZ ), result );
Nice start, and nice that this works with base types, too:
[Fact]
public void WorksWithBaseTypes()
{
double length1X = 1.0;
double length1Y = 2.0;
double length1Z = 3.0;
double length2X = 4.0;
double length2Y = 5.0;
double length2Z = 6.0;
var vector1 = new Vector3<double>(length1X, length1Y, length1Z);
var vector2 = new Vector3<double>(length2X, length2Y, length2Z);
var result = vector1 + vector2;
double expectedX = 5.0;
double expectedY = 7.0;
double expectedZ = 9.0;
Assert.Equal(new Vector3<double>(expectedX, expectedY, expectedZ), result);
}
This doesn't make a lot of sense, but I just tried to do:
[Fact]
public void ThisIsWeird()
{
var vector1 = new Vector3<String>("A", "B", "C");
var vector2 = new Vector3<String>("D", "E", "F");
Assert.Equal("AD", (vector1 + vector2).X);
}
Not that I would have a meaningful usage for this, but this failed with
System.TypeInitializationException : The type initializer for 'AddImplementation`3' threw an exception.
---- System.InvalidOperationException : The binary operator Add is not defined for the types 'System.String' and 'System.String'.
at UnitsNet.CompiledLambdas.AddImplementation`3.Invoke(TLeft left, TRight right) in C:\projects\UnitsNet\UnitsNet\CompiledLambdas.cs:line 224
at UnitsNet.CompiledLambdas.Add[T](T left, T right) in C:\projects\UnitsNet\UnitsNet\CompiledLambdas.cs:line 61
at UnitsNet.Vector3`1.op_Addition(Vector3`1 left, Vector3`1 right) in C:\projects\UnitsNet\UnitsNet\Vector3.cs:line 59
at UnitsNet.Tests.VectorTests.ThisIsWeird() in C:\projects\UnitsNet\UnitsNet.Tests\VectorTests.cs:line 73
String does have a binary + operator, so this message confuses me a bit and I'm worried we're overlooking some meaningful use case here?
String does not have a + operator 😃
It is syntactic sugar for the compiler to generate string.Concat.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
@krwq @pgrawehr What methods/operators would you like to see? I assume something similar to Vector3?
@tmilnthorp yes, something similar would be perfect. At minimum I think:
Note that things like Normalize and similar could not have units anymore so they would have to return regular Vector3
Yea, that sounds good. The Vector is not limited to 3 dimensions though, right? Shall we have the Matrix, too? If desired, I can provide the implementation for it. Seeing that System.Numerics doesn't seem to provide an arbitrary matrix class, this would be a plus.
@pgrawehr Vector in .NET is more about SIMD instructions. I currently don't see any larger short term benefits of having Matrix in IoT repo but Vector3 is being used quite a bit
Right, IOT will not necessarily benefit, but having a generic matrix class could come in handy in other places. Due to the limits in the .NET generics system (for which we seem to have a good workaround now), this probably hasn't been done yet.
This sparked my interest, since we're also using 3D vectors in our code (geometry related)
What I don't quite understand, is the added benefit of adding a vector object to UnitsNet? As @angularsen pointed out, it is very easy to create your own vector wrapper around the units that you need.
That is what we're doing now too and on top, we are wrapping MathNet.Spatial Vector3D along with it:
/// <summary>
/// Represents a vector in 3D space.
/// A vector describes the change in XYZ between two points in 3D space.
/// </summary>
public sealed class Vector : IEquatable<Vector>
{
private readonly Vector3D _vector3D;
private Vector(Vector3D vector)
{
_vector3D = vector;
IsUnitVector = false;
}
private Vector(UnitVector3D vector)
{
_vector3D = new Vector3D(vector.X, vector.Y, vector.Z);
IsUnitVector = true;
}
private Vector(Point3D point)
{
_vector3D = new Vector3D(point.X, point.Y, point.Z);
}
public Vector(double x, double y, double z)
: this(Length.FromMeters(x), Length.FromMeters(y), Length.FromMeters(z)) { }
public Vector(Length? x, Length? y, Length? z)
: this(x ?? Length.Zero, y ?? Length.Zero, z ?? Length.Zero) { }
public Vector(Length x, Length y, Length z)
{
_vector3D = new Vector3D(x.Meters, y.Meters, z.Meters);
IsUnitVector = false;
}
public Length X => Length.FromMeters(_vector3D.X);
public Length Y => Length.FromMeters(_vector3D.Y);
public Length Z => Length.FromMeters(_vector3D.Z);
public bool IsUnitVector { get; }
public double Magnitude => _vector3D.Length;
public Vector Orthogonal => new Vector(_vector3D.Orthogonal);
public Vector UnitVector
{
get
{
if (this.IsUnitVector)
{
return this;
}
return new Vector(_vector3D.Normalize());
}
}
internal object GetUnderlyingObject()
{
return _vector3D;
}
public bool IsVertical
{
get
{
return Math.Abs(X.Meters) < 1e-3 && Math.Abs(Y.Meters) < 1e-3;
}
}
public Vector Rotate(Vector about, Angle angle)
{
UnitVector3D normalizedAbout = about.IsUnitVector ? UnitVector3D.Create(about.X.Meters, about.Y.Meters, about.Z.Meters) : ((Vector3D)about.GetUnderlyingObject()).Normalize();
Vector3D rotatedVector =
((Vector3D) GetUnderlyingObject()).Rotate(normalizedAbout,
MathNet.Spatial.Units.Angle.FromRadians(angle.Radians));
return new Vector(rotatedVector);
}
public static Vector Zero => new Vector(0, 0, 0);
#region Operators
/// <summary>
/// Add 2 vectors together, resulting in a new vector
/// </summary>
/// <param name="first">A vector</param>
/// <param name="second">Another vector</param>
/// <returns>A new vector which is the result of both vectors combined.
/// This vector describes the change in X, Y and Z to go from the start of the first vector to the end of the second vector.</returns>
public static Vector operator +(Vector first, Vector second)
{
var vector1 = (Vector3D) first.GetUnderlyingObject();
var vector2 = (Vector3D) second.GetUnderlyingObject();
Vector3D result = vector1 + vector2;
return new Vector(result);
}
/// <summary>
/// Subtract 2 vectors from each other, resulting in a new vector
/// </summary>
/// <param name="first">A vector</param>
/// <param name="second">Another vector</param>
/// <returns>A new vector which is the result of both vectors subtracted from eachother.
/// This vector describes the change in X, Y and Z to go from the end of the second vector to the end of the first vector</returns>
public static Vector operator -(Vector first, Vector second)
{
var vector1 = (Vector3D)first.GetUnderlyingObject();
var vector2 = (Vector3D)second.GetUnderlyingObject();
Vector3D result = vector1 - vector2;
return new Vector(result);
}
/// <summary>
/// Negate a vector, reversing its direction
/// </summary>
/// <param name="vectorToNegate">The vector to negate</param>
/// <returns></returns>
public static Vector operator -(Vector vectorToNegate)
{
var vector = (Vector3D) vectorToNegate.GetUnderlyingObject();
Vector3D result = vector.Negate();
return new Vector(result);
}
/// <summary>
/// Create a unit vector from the provided vector. (= vector normalization)
/// </summary>
/// <param name="vectorToNormalize">The vector that needs to be normalized</param>
/// <returns>A new vector which is the unit vector of the provided vector.
/// This vector will have a magnitude / size of 1 but the same direction as the provided vector.</returns>
public static Vector operator !(Vector vectorToNormalize)
{
if (vectorToNormalize.IsUnitVector)
{
return vectorToNormalize;
}
var vector = (Vector3D) vectorToNormalize.GetUnderlyingObject();
UnitVector3D result = vector.Normalize();
return new Vector(result);
}
/// <summary>
/// Perform scalar division on a vector (= scale a vector)
/// If the scalar value is bigger than 1, it will change the magnitude of the vector (make it shorter)
/// If the scalar value is lesser than 1 but bigger than 0, it will change the magnitude of the vector (make it longer)
/// If the scalar value is bigger than -1 but lesser than 0, it will reverse the direction and change its magnitude (make it longer)
/// If the scalar value is -1, it will reverse the direction of the vector but not change its magnitude
/// If the scalar value is lesser than -1, it will reverse the direction and change its magnitude (make it shorter)
/// </summary>
/// <param name="vectorToScale">The vector to scale</param>
/// <param name="scalar">The scalar value to scale the vector with</param>
/// <returns>A new vector which is the scaled version of the provided vector</returns>
public static Vector operator /(Vector vectorToScale, double scalar)
{
var vector = (Vector3D)vectorToScale.GetUnderlyingObject();
Vector3D result = vector.ScaleBy(1/scalar);
return new Vector(result);
}
/// <summary>
/// Perform scalar multiplication on a vector (= scale a vector).
/// If the scalar value is bigger than 1, it will change the magnitude of the vector (make it longer).
/// If the scalar value is -1, it will reverse the direction of the vector but not change its magnitude
/// If the scalar value is lesser than -1, it will reverse the direction and change the magnitude of the vector (make it longer).
/// </summary>
/// <param name="vectorToScale">The vector to scale</param>
/// <param name="scalar">The scalar value to scale the vector with</param>
/// <returns>A new vector which is the scaled version of the provided vector</returns>
public static Vector operator *(Vector vectorToScale, double scalar)
{
var vector = (Vector3D) vectorToScale.GetUnderlyingObject();
Vector3D result = vector.ScaleBy(scalar);
return new Vector(result);
}
/// <summary>
/// Perform scalar multiplication on a vector (= scale a vector).
/// If the scalar value is bigger than 1, it will change the magnitude of the vector (make it longer).
/// If the scalar value is -1, it will reverse the direction of the vector but not change its magnitude
/// If the scalar value is lesser than -1, it will reverse the direction and change the magnitude of the vector (make it longer).
/// </summary>
/// <param name="scalar">The scalar value to scale the vector with</param>
/// <param name="vectorToScale">The vector to scale</param>
/// <returns>A new vector which is the scaled version of the provided vector</returns>
public static Vector operator *(double scalar, Vector vectorToScale)
{
return vectorToScale * scalar;
}
/// <summary>
/// Performs the vector cross product operation.
///
/// </summary>
/// <param name="first">The first vector to use in the vector cross product operation</param>
/// <param name="second">The second vector to use in the vector cross product operation</param>
/// <returns>A new vector which is orthogonal (90° angle) to both vectors</returns>
public static Vector operator *(Vector first, Vector second)
{
var vector1 = (Vector3D) first.GetUnderlyingObject();
var vector2 = (Vector3D) second.GetUnderlyingObject();
Vector3D result = vector1.CrossProduct(vector2);
return new Vector(result);
}
/// <summary>
/// Calculate the magnitude (= size) of the provided vector
/// </summary>
/// <param name="vectorToCalculateSizeFor">The vector of which we want to know the size</param>
/// <returns>A scalar value representing the magnitude of the vector</returns>
public static double operator ~(Vector vectorToCalculateSizeFor)
{
var vector = (Vector3D)vectorToCalculateSizeFor.GetUnderlyingObject();
return vector.Length;
}
/// <summary>
/// Performs the vector dot product operation
/// </summary>
/// <param name="first">The first vector to use in the vector dot product operation</param>
/// <param name="second">The second vector to use in th vector dot product operation</param>
/// <returns>A scalar value
/// If the value is zero, it means the angle between the 2 provided vectors is 90° (which means they are orthogonal / perpendicular)
/// </returns>
public static double operator |(Vector first, Vector second)
{
var vector1 = (Vector3D)first.GetUnderlyingObject();
var vector2 = (Vector3D)second.GetUnderlyingObject();
return vector1.DotProduct(vector2);
}
/// <summary>
/// Convert a Point to a vector
/// </summary>
/// <param name="point">The point to convert to a vector</param>
public static explicit operator Vector(Point point)
{
var point3d = (Point3D) point.GetUnderlyingObject();
return new Vector(point3d);
}
public static explicit operator Vector(Vector3D mathNetVector)
{
return new Vector(mathNetVector);
}
/// <summary>
/// Checks if both vectors are equal
/// </summary>
/// <param name="first">The first vector to compare with</param>
/// <param name="second">The second vector to compare against</param>
/// <returns>True if both vectors are equal, false otherwise</returns>
public static bool operator ==(Vector first, Vector second)
{
if (ReferenceEquals(first, null))
{
return ReferenceEquals(second, null);
}
return first.Equals(second);
}
/// <summary>
/// Checks if both vectors are not equal
/// </summary>
/// <param name="first">The first vector to compare with</param>
/// <param name="second">The second vector to compare against</param>
/// <returns>True if both vector are not equal, false otherwise</returns>
public static bool operator !=(Vector first, Vector second)
{
if (ReferenceEquals(first, null))
{
return !ReferenceEquals(second, null);
}
return !first.Equals(second);
}
#endregion
#region IEquatable implementation
/// <summary>
/// Checks if this instance is equal to the provided instance
/// </summary>
/// <param name="other">The other instance to compare against</param>
/// <returns>True if both instances are equal</returns>
public bool Equals(Vector other)
{
if (other is null)
{
return false;
}
var otherVector3D = (Vector3D) other.GetUnderlyingObject();
return _vector3D.Equals(otherVector3D, 1E-2);
}
/// <summary>
/// Checks if this instance is equal to the provided object
/// </summary>
/// <param name="obj">The object to compare against</param>
/// <returns>True if both instances are equal</returns>
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj))
{
return false;
}
if (ReferenceEquals(this, obj))
{
return true;
}
if (obj.GetType() != this.GetType())
{
return false;
}
return Equals((Vector) obj);
}
/// <summary>
/// Calculates the hashcode of this instance.
/// </summary>
/// <returns>The hashcode of this object</returns>
public override int GetHashCode()
{
return _vector3D.GetHashCode();
}
#endregion
/// <summary>
/// Returns a user-friendly string representation of this vector in 3D space (X, Y, Z)
/// </summary>
/// <returns>A string</returns>
public override string ToString()
{
return $"({X}, {Y}, {Z})";
}
}
@dschuermans Your implementation is not generic (but only for Lenght in this case). We want to be able to define Vector<T>
, which is not a simple task, due to limitations of how .NET generics work.
@pgrawehr So my point remains: what's the benefit of adding a vector object to UnitsNet? In all other libraries that implement a Vector object, it is unitless. If there is any unit assigned to it, it would be length It's impossible to have a vector in which X,Y or Z values are different units.
As far as I understand Vectors, it is their values that matter and the result of mathematic operations on them. Whether you say that the X, Y and Z values are in meters, magnetic field or hell, even cows doesn't matter for the functionality of the vector. The resulting numbers will remain the same. I'd even dare to say, that in the end it all boils down to Length:
Vectors are a geometric way of representing quantities that have direction as well as magnitude. An example of a vector is force. If we are to fully describe a force on an object we need to specify not only how much force is applied, but also in which direction. Another example of a vector quantity is velocity -- an object that is traveling at ten meters per second to the east has a different velocity than an object that is traveling ten meters per second to the west. This vector is a special case, however, sometimes people are interested in only the magnitude of the velocity of an object. This quantity, a scalar, is called speed which has magnitude but no given direction. When vectors are written, they are represented by a single letter in bold type or with an arrow above the letter. Some examples of vectors are displacement (e.g. 120 cm at 30°) and velocity (e.g. 12 meters per second north). The only basic SI unit that is a vector is the meter. All others are scalars. Derived quantities can be vector or scalar, but every vector quantity must involve meters in its definition and unit.
So wouldn't a solution for this issue be that you have a unitless vector, yet with some functionality to convert it to a set of X, Y and Z values in a specific unit?
using System;
using UnitsNet;
public class Program
{
public static void Main()
{
var vector1 = new Vector(1, 1, 1);
var vector2 = new Vector(1, 1, 1);
var result = vector1 + vector2;
Console.WriteLine(result.AsUnit<Length>());
Console.WriteLine(result.AsUnit<MagneticField>());
}
}
public sealed class Vector {
public double X {get;set;}
public double Y {get;set;}
public double Z {get;set;}
public Vector(double x, double y, double z){
X = x;
Y = y;
Z = z;
}
public (T X, T Y, T Z) AsUnit<T>()
where T: IQuantity
{
var temp = Activator.CreateInstance<T>();
T x = (T)Quantity.From(X, temp.QuantityInfo.BaseUnit);
T y = (T)Quantity.From(X, temp.QuantityInfo.BaseUnit);
T z = (T)Quantity.From(X, temp.QuantityInfo.BaseUnit);
return (x, y , z);
}
public static Vector operator +(Vector first, Vector second)
{
return new Vector(first.X + second.X, first.Y + second.Y, first.Z + second.Z);
}
}
Output:
(2 m, 2 m, 2 m)
(2 T, 2 T, 2 T)
With a unitless vector, you can fallback onto libraries that specialize in this stuff (e.g. MathNet), instead of reinventing the wheel and creating your own Vector implementation again.
The suggestion above could solve the original issue, that's true. I'll take a look at that MathNet library (I don't know that one), whether it otherwise supports what we need. However, I disagree that it's always length. I've used vectors at least for length, speed, orientation (angles), and force.
Having a generic Vector<T>
type on the other hand does have some advantages, though:
Earlier in this issue there are several links to proof of concepts and proposals for a generic way to do arithmetic of quantities, which would be transferable to vectors of quantities. It seems doable, but someone needs to champion it and start a PR to drive that forward.
Without this, there are today several feasible alternatives:
Length3
, Mass3
vector types and implement the overloads@angularsen Now that .net 6 supports generic math, maybe it is easier to do this kind of stuff? https://devblogs.microsoft.com/dotnet/preview-features-in-net-6-generic-math/
Well hello there Mr Krog :D
Yes I think so. We touched on it in #977, but it's always hard to see the possibilities before trying a bit.
I threw up #984 just to play with it.
INumber
does not seem like a good match for quantities, it's the whole kitchen sink and then some.
Temperature
have different zero points for Kelvin, Celsius and Fahrenheit). We can opt-in for quantities that do support it, by adding the interface only to those.INumber.One
.)Value
, but again, crossing into non-intuitive behavior.Fortunately they have split that up into fine grained interfaces like IAdditionOperators
and IAdditiveIdentity
and also supports different types of TSelf
and TResult
. Nice!
I'm pretty sure we will make use of this when it leaves preview.
cc: @tannergooding
We're currently considering to use this library in https://github.com/dotnet/iot/ . We have initially started developing very similar APIs and then noticed this library so we decided to give it a try since it already solves many problems we were about to hit.
We have converted our Temperature/Pressure structs to this library without any issues (see https://github.com/dotnet/iot/pull/1052) but now we're trying to use 3-dimensional magnetic field which is the output of IMU (i.e. BNO055 sensor). We haven't ourselves developed such unit yet and used Vector3.
What's the recommended way to achieve 3-dimensional magnetic field?