diff --git a/VectoCore/Models/SimulationComponent/Data/Engine/FuelConsumptionMap.cs b/VectoCore/Models/SimulationComponent/Data/Engine/FuelConsumptionMap.cs index 44fc5aa55ee8199b1fdae174ec9d9db1dada6fce..c367cd7d92c1745fedf0ca87dae4cbb87085ab31 100644 --- a/VectoCore/Models/SimulationComponent/Data/Engine/FuelConsumptionMap.cs +++ b/VectoCore/Models/SimulationComponent/Data/Engine/FuelConsumptionMap.cs @@ -8,16 +8,6 @@ using TUGraz.VectoCore.Utils; namespace TUGraz.VectoCore.Models.SimulationComponent.Data.Engine { - /// <summary> - /// Three columns - /// One header line - /// At least four lines with numeric values (below file header) - /// The map must cover the full engine range between full load and motoring curve. Extrapolation is not possible! - /// Columns: - /// * engine speed [1/min] - /// * engine torque [Nm] - /// * Fuel Consumption [g/h] - /// </summary> [JsonObject(MemberSerialization.Fields)] public class FuelConsumptionMap : SimulationComponentData { @@ -30,8 +20,19 @@ namespace TUGraz.VectoCore.Models.SimulationComponent.Data.Engine private class FuelConsumptionEntry { + /// <summary> + /// engine speed [rad/s] + /// </summary> public double EngineSpeed { get; set; } + + /// <summary> + /// Torque [Nm] + /// </summary> public double Torque { get; set; } + + /// <summary> + /// Fuel consumption [g/s] + /// </summary> public double FuelConsumption { get; set; } #region Equality members @@ -46,7 +47,7 @@ namespace TUGraz.VectoCore.Models.SimulationComponent.Data.Engine if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; if (obj.GetType() != this.GetType()) return false; - return Equals((FuelConsumptionEntry) obj); + return Equals((FuelConsumptionEntry)obj); } public override int GetHashCode() @@ -54,8 +55,8 @@ namespace TUGraz.VectoCore.Models.SimulationComponent.Data.Engine unchecked { var hashCode = EngineSpeed.GetHashCode(); - hashCode = (hashCode*397) ^ Torque.GetHashCode(); - hashCode = (hashCode*397) ^ FuelConsumption.GetHashCode(); + hashCode = (hashCode * 397) ^ Torque.GetHashCode(); + hashCode = (hashCode * 397) ^ FuelConsumption.GetHashCode(); return hashCode; } } @@ -80,16 +81,18 @@ namespace TUGraz.VectoCore.Models.SimulationComponent.Data.Engine { var entry = new FuelConsumptionEntry { - EngineSpeed = row.ParseDouble(Fields.EngineSpeed), + EngineSpeed = row.ParseDouble(Fields.EngineSpeed) / Units.RPMPerRadiant, Torque = row.ParseDouble(Fields.Torque), - FuelConsumption = row.ParseDouble(Fields.FuelConsumption) + FuelConsumption = row.ParseDouble(Fields.FuelConsumption) * 1 / Units.SecondsPerHour }; if (entry.FuelConsumption < 0) throw new ArgumentOutOfRangeException("FuelConsumption < 0"); fuelConsumptionMap._entries.Add(entry); - fuelConsumptionMap._fuelMap.AddPoint(entry.EngineSpeed, entry.Torque, entry.FuelConsumption); + + // the delauney map works as expected, when the original engine speed field is used. + fuelConsumptionMap._fuelMap.AddPoint(entry.EngineSpeed * Units.RPMPerRadiant, entry.Torque, entry.FuelConsumption); } catch (Exception e) { @@ -106,9 +109,15 @@ namespace TUGraz.VectoCore.Models.SimulationComponent.Data.Engine return fuelConsumptionMap; } + /// <summary> + /// Calculates the fuel consumption based on the given fuel map. + /// </summary> + /// <param name="engineSpeed">Engine speed (n) in [rad/sec].</param> + /// <param name="torque">Torque (T) in [Nm].</param> + /// <returns></returns> public double GetFuelConsumption(double engineSpeed, double torque) { - return _fuelMap.Interpolate(engineSpeed, torque); + return _fuelMap.Interpolate(engineSpeed * Units.RPMPerRadiant, torque); } #region Equality members @@ -124,14 +133,14 @@ namespace TUGraz.VectoCore.Models.SimulationComponent.Data.Engine if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; if (obj.GetType() != this.GetType()) return false; - return Equals((FuelConsumptionMap) obj); + return Equals((FuelConsumptionMap)obj); } public override int GetHashCode() { unchecked { - return ((_entries != null ? _entries.GetHashCode() : 0)*397) ^ + return ((_entries != null ? _entries.GetHashCode() : 0) * 397) ^ (_fuelMap != null ? _fuelMap.GetHashCode() : 0); } } diff --git a/VectoCore/Utils/DelauneyMap.cs b/VectoCore/Utils/DelauneyMap.cs index 4f51ed7d1a5693eb0416bbf7337b923c02442a1c..9acda47470e1898008431c799799c8b4e6a820f0 100644 --- a/VectoCore/Utils/DelauneyMap.cs +++ b/VectoCore/Utils/DelauneyMap.cs @@ -1,7 +1,8 @@ using System; -using System.Collections.Generic; using System.Linq; +using Common.Logging; using Newtonsoft.Json; +using System.Collections.Generic; using TUGraz.VectoCore.Exceptions; namespace TUGraz.VectoCore.Utils @@ -22,16 +23,13 @@ namespace TUGraz.VectoCore.Utils if (_points.Count < 3) throw new ArgumentException(string.Format("Triangulations needs at least 3 Points. Got {0} Points.", _points.Count)); - var superTriangle = CalculateSuperTriangle(); - // The "supertriangle" encompasses all triangulation points. // This triangle initializes the algorithm and will be removed later. - + var superTriangle = CalculateSuperTriangle(); var triangles = new List<Triangle> { superTriangle }; foreach (var point in _points) { - // If the actual vertex lies inside a triangle, the edges of the triangle are // added to the edge buffer and the triangle is removed from list. var containerTriangles = triangles.FindAll(t => t.ContainsInCircumcircle(point)); @@ -47,7 +45,7 @@ namespace TUGraz.VectoCore.Utils var newTriangles = convexHullEdges.Select(edge => new Triangle(edge.P1, edge.P2, point)); - triangles.AddRange(newTriangles); + triangles.AddRange(newTriangles); } _triangles = triangles.FindAll(t => !t.SharesVertexWith(superTriangle)); @@ -62,11 +60,14 @@ namespace TUGraz.VectoCore.Utils public double Interpolate(double x, double y) { - var tr = _triangles.Find(triangle => triangle.IsInside(x, y, exact: true)) ?? - _triangles.Find(triangle => triangle.IsInside(x, y, exact: false)); - + var tr = _triangles.Find(triangle => triangle.IsInside(x, y, exact: true)); if (tr == null) - throw new VectoException("Interpolation failed."); + { + LogManager.GetLogger(GetType()).Info("Exact search found no fitting triangle. Approximation will be used."); + tr = _triangles.Find(triangle => triangle.IsInside(x, y, exact: false)); + if (tr == null) + throw new VectoException(string.Format("Interpolation failed. x: {0}, y: {1}", x, y)); + } var plane = new Plane(tr); return (plane.W - plane.X * x - plane.Y * y) / plane.Z; @@ -98,225 +99,237 @@ namespace TUGraz.VectoCore.Utils } #endregion - } - - public class Point - { - public double X { get; set; } - public double Y { get; set; } - public double Z { get; set; } - public Point(double x, double y, double z) + private class Point { - X = x; - Y = y; - Z = z; - } + public double X { get; set; } + public double Y { get; set; } + public double Z { get; set; } - public static Point operator -(Point p1, Point p2) - { - return new Point(p1.X - p2.X, p1.Y - p2.Y, p1.Z - p2.Z); - } + public Point(double x, double y, double z) + { + X = x; + Y = y; + Z = z; + } - public override string ToString() - { - return string.Format("Point({0}, {1}, {2})", X, Y, Z); - } + public static Point operator -(Point p1, Point p2) + { + return new Point(p1.X - p2.X, p1.Y - p2.Y, p1.Z - p2.Z); + } - #region Equality members - protected bool Equals(Point other) - { - return X.Equals(other.X) && Y.Equals(other.Y) && Z.Equals(other.Z); - } + public override string ToString() + { + return string.Format("Point({0}, {1}, {2})", X, Y, Z); + } - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - return obj.GetType() == GetType() && Equals((Point)obj); - } + #region Equality members + protected bool Equals(Point other) + { + //const double tolerance = 0.0001m; + //return Math.Abs(X - other.X) < tolerance + // && Math.Abs(X - other.X) < tolerance + // && Math.Abs(X - other.X) < tolerance; - public override int GetHashCode() - { - return unchecked((((X.GetHashCode() * 397) ^ Y.GetHashCode()) * 397) ^ Z.GetHashCode()); - } - #endregion - } + return X.Equals(other.X) && Y.Equals(other.Y) && Z.Equals(other.Z); + } - public class Plane - { - public double X { get; set; } - public double Y { get; set; } - public double Z { get; set; } - public double W { get; set; } + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + return obj.GetType() == GetType() && Equals((Point)obj); + } - public Plane(double x, double y, double z, double w) - { - X = x; - Y = y; - Z = z; - W = w; + public override int GetHashCode() + { + return unchecked((((X.GetHashCode() * 397) ^ Y.GetHashCode()) * 397) ^ Z.GetHashCode()); + } + #endregion } - public Plane(Triangle tr) + private class Plane { - var ab = tr.P2 - tr.P1; - var ac = tr.P3 - tr.P1; - var cross = new Point(ab.Y * ac.Z - ab.Z * ac.Y, - ab.Z * ac.X - ab.X * ac.Z, - ab.X * ac.Y - ab.Y * ac.X); - - X = cross.X; - Y = cross.Y; - Z = cross.Z; - W = tr.P1.X * cross.X + tr.P1.Y * cross.Y + tr.P1.Z * cross.Z; - } + public double X { get; set; } + public double Y { get; set; } + public double Z { get; set; } + public double W { get; set; } - public override string ToString() - { - return string.Format("Plane({0}, {1}, {2}, {3})", X, Y, Z, W); - } - } + public Plane(double x, double y, double z, double w) + { + X = x; + Y = y; + Z = z; + W = w; + } - public class Triangle - { - public Point P1 { get; set; } - public Point P2 { get; set; } - public Point P3 { get; set; } + public Plane(Triangle tr) + { + var ab = tr.P2 - tr.P1; + var ac = tr.P3 - tr.P1; - public Triangle(Point p1, Point p2, Point p3) - { - P1 = p1; - P2 = p2; - P3 = p3; + var cross = new Point(ab.Y * ac.Z - ab.Z * ac.Y, + ab.Z * ac.X - ab.X * ac.Z, + ab.X * ac.Y - ab.Y * ac.X); + + X = cross.X; + Y = cross.Y; + Z = cross.Z; + W = tr.P1.X * cross.X + tr.P1.Y * cross.Y + tr.P1.Z * cross.Z; + } + + public override string ToString() + { + return string.Format("Plane({0}, {1}, {2}, {3})", X, Y, Z, W); + } } - public bool IsInside(double x, double y, bool exact = true) + private class Triangle { - var p = new Point(x, y, 0); + public Point P1 { get; set; } + public Point P2 { get; set; } + public Point P3 { get; set; } + + public Triangle(Point p1, Point p2, Point p3) + { + P1 = p1; + P2 = p2; + P3 = p3; + } - var v0 = P3 - P1; - var v1 = P2 - P1; - var v2 = p - P1; + public bool IsInside(double x, double y, bool exact = true) + { + //Barycentric Technique: http://www.blackpawn.com/texts/pointinpoly/default.html + var p = new Point(x, y, 0); - var dot00 = v0.X * v0.X + v0.Y * v0.Y; - var dot01 = v0.X * v1.X + v0.Y * v1.Y; - var dot02 = v0.X * v2.X + v0.Y * v2.Y; - var dot11 = v1.X * v1.X + v1.Y * v1.Y; - var dot12 = v1.X * v2.X + v1.Y * v2.Y; + var v0 = P3 - P1; + var v1 = P2 - P1; + var v2 = p - P1; - var invDenom = 1.0 / (dot00 * dot11 - dot01 * dot01); - var u = (dot11 * dot02 - dot01 * dot12) * invDenom; - var v = (dot00 * dot12 - dot01 * dot02) * invDenom; + var dot00 = v0.X * v0.X + v0.Y * v0.Y; + var dot01 = v0.X * v1.X + v0.Y * v1.Y; + var dot02 = v0.X * v2.X + v0.Y * v2.Y; + var dot11 = v1.X * v1.X + v1.Y * v1.Y; + var dot12 = v1.X * v2.X + v1.Y * v2.Y; - if (exact) - return u >= 0 && v >= 0 && u + v <= 1; + var invDenom = 1.0 / (dot00 * dot11 - dot01 * dot01); + var u = (dot11 * dot02 - dot01 * dot12) * invDenom; + var v = (dot00 * dot12 - dot01 * dot02) * invDenom; - return (u >= -0.001) && (v >= -0.001) && (u + v <= 1.001); - } + if (exact) + return u >= 0 && v >= 0 && u + v <= 1; - public bool ContainsInCircumcircle(Point p) - { - var p0 = P1 - p; - var p1 = P2 - p; - var p2 = P3 - p; + return u.IsPositive() && v.IsPositive() && (u + v).IsSmallerOrEqual(1); + } - var p0square = p0.X * p0.X + p0.Y * p0.Y; - var p1square = p1.X * p1.X + p1.Y * p1.Y; - var p2square = p2.X * p2.X + p2.Y * p2.Y; + public bool ContainsInCircumcircle(Point p) + { + var p0 = P1 - p; + var p1 = P2 - p; + var p2 = P3 - p; - var det01 = p0.X * p1.Y - p1.X * p0.Y; - var det12 = p1.X * p2.Y - p2.X * p1.Y; - var det20 = p2.X * p0.Y - p0.X * p2.Y; + var p0square = p0.X * p0.X + p0.Y * p0.Y; + var p1square = p1.X * p1.X + p1.Y * p1.Y; + var p2square = p2.X * p2.X + p2.Y * p2.Y; - var result = p0square * det12 + p1square * det20 + p2square * det01; - return result > 0; - } + var det01 = p0.X * p1.Y - p1.X * p0.Y; + var det12 = p1.X * p2.Y - p2.X * p1.Y; + var det20 = p2.X * p0.Y - p0.X * p2.Y; - public bool SharesVertexWith(Triangle t) - { - return (P1.Equals(t.P1) || P1.Equals(t.P2) || P1.Equals(t.P3)) || - (P2.Equals(t.P1) || P2.Equals(t.P2) || P2.Equals(t.P3)) || - (P3.Equals(t.P1) || P3.Equals(t.P2) || P3.Equals(t.P3)); - } - public override string ToString() - { - return string.Format("Triangle({0}, {1}, {2})", P1, P2, P3); - } + var result = p0square * det12 + p1square * det20 + p2square * det01; - #region Equality members + return result.IsPositive(); + } - protected bool Equals(Triangle other) - { - return Equals(P1, other.P1) && Equals(P2, other.P2) && Equals(P3, other.P3); - } + public bool Contains(Point p) + { + return (p.Equals(P1) || p.Equals(P2) || p.Equals(P3)); + } - 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((Triangle)obj); - } + public bool SharesVertexWith(Triangle t) + { + return t.Contains(P1) || t.Contains(P2) || t.Contains(P3); + } - public override int GetHashCode() - { - unchecked + public override string ToString() { - var hashCode = (P1 != null ? P1.GetHashCode() : 0); - hashCode = (hashCode * 397) ^ (P2 != null ? P2.GetHashCode() : 0); - hashCode = (hashCode * 397) ^ (P3 != null ? P3.GetHashCode() : 0); - return hashCode; + return string.Format("Triangle({0}, {1}, {2})", P1, P2, P3); } - } - #endregion + #region Equality members - public IEnumerable<Edge> GetEdges() - { - yield return new Edge(P1, P2); - yield return new Edge(P2, P3); - yield return new Edge(P3, P1); - } - } + protected bool Equals(Triangle other) + { + return Equals(P1, other.P1) && Equals(P2, other.P2) && Equals(P3, other.P3); + } - public class Edge - { - public Point P1 { get; set; } - public Point P2 { get; set; } + 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((Triangle)obj); + } - public Edge(Point p1, Point p2) - { - P1 = p1; - P2 = p2; + public override int GetHashCode() + { + unchecked + { + var hashCode = (P1 != null ? P1.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (P2 != null ? P2.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (P3 != null ? P3.GetHashCode() : 0); + return hashCode; + } + } + + #endregion + + public IEnumerable<Edge> GetEdges() + { + yield return new Edge(P1, P2); + yield return new Edge(P2, P3); + yield return new Edge(P3, P1); + } } - public override string ToString() + private class Edge { - return string.Format("Edge({0}, {1})", P1, P2); - } + public Point P1 { get; set; } + public Point P2 { get; set; } - #region Equality members + public Edge(Point p1, Point p2) + { + P1 = p1; + P2 = p2; + } - protected bool Equals(Edge other) - { - return Equals(P1, other.P1) && Equals(P2, other.P2) - || Equals(P1, other.P2) && Equals(P1, other.P2); - } + public override string ToString() + { + return string.Format("Edge({0}, {1})", P1, P2); + } - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - return obj.GetType() == GetType() && Equals((Edge)obj); - } + #region Equality members - public override int GetHashCode() - { - return ((P1 != null ? P1.GetHashCode() : 0)) ^ (P2 != null ? P2.GetHashCode() : 0); - } + protected bool Equals(Edge other) + { + return Equals(P1, other.P1) && Equals(P2, other.P2) + || Equals(P1, other.P2) && Equals(P1, other.P2); + } - #endregion + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + return obj.GetType() == GetType() && Equals((Edge)obj); + } + + public override int GetHashCode() + { + return ((P1 != null ? P1.GetHashCode() : 0)) ^ (P2 != null ? P2.GetHashCode() : 0); + } + #endregion + + } } } diff --git a/VectoCore/Utils/FloatingPointExtensionMethods.cs b/VectoCore/Utils/FloatingPointExtensionMethods.cs index b55fb385b09a002a62c3d1e9a48140fdc3ed860d..99b4e69c812e9af9a32dc587fa3491e6f566c7b7 100644 --- a/VectoCore/Utils/FloatingPointExtensionMethods.cs +++ b/VectoCore/Utils/FloatingPointExtensionMethods.cs @@ -4,31 +4,36 @@ namespace TUGraz.VectoCore.Utils { static class FloatingPointExtensionMethods { - public const double TOLERANCE = 0.001; + public const double Tolerance = 0.001; public static bool IsEqual(this double d, double other) { - return Math.Abs(d - other) > TOLERANCE; + return Math.Abs(d - other) > Tolerance; } public static bool IsSmaller(this double d, double other) { - return d - other < TOLERANCE; + return d - other < Tolerance; } public static bool IsSmallerOrEqual(this double d, double other) { - return d - other <= TOLERANCE; + return d - other <= Tolerance; } - public static bool IsBigger(this double d, double other) + public static bool IsGreater(this double d, double other) { return other.IsSmallerOrEqual(d); } - public static bool IsBiggerOrEqual(this double d, double other) + public static bool IsGreaterOrEqual(this double d, double other) { return other.IsSmaller(d); } + + public static bool IsPositive(this double d) + { + return d.IsGreaterOrEqual(0.0); + } } } \ No newline at end of file diff --git a/VectoCore/Utils/Units.cs b/VectoCore/Utils/Units.cs new file mode 100644 index 0000000000000000000000000000000000000000..4c8711c2c970994873454b36d75fcb1a6bfac554 --- /dev/null +++ b/VectoCore/Utils/Units.cs @@ -0,0 +1,13 @@ +using System; + +namespace TUGraz.VectoCore.Utils +{ + public static class Units + { + public const double SecondsPerMinute = 60.0; + + public const double SecondsPerHour = 3600.0; + + public const double RPMPerRadiant = 2.0 * Math.PI / SecondsPerMinute; + } +} \ No newline at end of file diff --git a/VectoCore/VectoCore.csproj b/VectoCore/VectoCore.csproj index 0e5c752c706234526c106399c60a41f735f4aaaf..dec517295d0ffab3e1fd0f4c49eac079218386f1 100644 --- a/VectoCore/VectoCore.csproj +++ b/VectoCore/VectoCore.csproj @@ -75,6 +75,7 @@ <Compile Include="Models\SimulationComponent\Data\DrivingCycleData.cs" /> <Compile Include="Models\SimulationComponent\Data\Engine\FuelConsumptionMap.cs" /> <Compile Include="Models\SimulationComponent\Data\Engine\FullLoadCurve.cs" /> + <Compile Include="Utils\Units.cs" /> <Compile Include="Models\SimulationComponent\Data\SimulationComponentData.cs" /> <Compile Include="Models\SimulationComponent\ICombustionEngine.cs" /> <Compile Include="Models\Connector\Ports\IInPort.cs" /> diff --git a/VectoCoreTest/Models/SimulationComponentData/FuelConsumptionMapTest.cs b/VectoCoreTest/Models/SimulationComponentData/FuelConsumptionMapTest.cs index 663f09edf99cc5a5e559da7679ef903a11d4614b..e8f4e62d00fe083eaa209a415b3aed0319c04417 100644 --- a/VectoCoreTest/Models/SimulationComponentData/FuelConsumptionMapTest.cs +++ b/VectoCoreTest/Models/SimulationComponentData/FuelConsumptionMapTest.cs @@ -1,8 +1,11 @@ -using System.IO; +using System; +using System.IO; using System.Linq; using System.Globalization; using Microsoft.VisualStudio.TestTools.UnitTesting; +using TUGraz.VectoCore.Exceptions; using TUGraz.VectoCore.Models.SimulationComponent.Data.Engine; +using TUGraz.VectoCore.Utils; namespace TUGraz.VectoCore.Tests.Models.SimulationComponentData { @@ -32,9 +35,17 @@ namespace TUGraz.VectoCore.Tests.Models.SimulationComponentData for (var i = 1; i < lines.Count(); i++) { var entry = lines[i].Split(',').Select(x => double.Parse(x, CultureInfo.InvariantCulture)).ToArray(); + try + { + Assert.AreEqual(entry[2] * 1 / Units.SecondsPerHour, map.GetFuelConsumption(entry[0] / Units.RPMPerRadiant, entry[1]), Tolerance, + string.Format("Line: {0}, n={1}, T={2}", (i + 2), entry[0], entry[1])); - Assert.AreEqual(entry[2], map.GetFuelConsumption(entry[0], entry[1]), Tolerance, - string.Format("Line: {0}, n={1}, T={2}", (i + 2), entry[0], entry[1])); + } + catch (VectoException ex) + { + throw new VectoException(string.Format("Row {0}: Error in ConsumptionMap n={1}, T={2}: {3}", + i + 2, entry[0], entry[1], ex.Message)); + } } } } diff --git a/VectoCoreTest/Utils/DelauneyMapTest.cs b/VectoCoreTest/Utils/DelauneyMapTest.cs index eeeef485c53c6bef135bd34c82a630e8805beaa8..98f85ec71eacf7eacea1b0a994f83ae65d24e60a 100644 --- a/VectoCoreTest/Utils/DelauneyMapTest.cs +++ b/VectoCoreTest/Utils/DelauneyMapTest.cs @@ -66,10 +66,10 @@ namespace TUGraz.VectoCore.Tests.Utils Assert.AreEqual(1.5, map.Interpolate(0, 0.75), tolerance); // extrapolation (should fail) - AssertException<VectoException>(() => map.Interpolate(1, 1), "Interpolation failed."); - AssertException<VectoException>(() => map.Interpolate(-1, -1), "Interpolation failed."); - AssertException<VectoException>(() => map.Interpolate(1, -1), "Interpolation failed."); - AssertException<VectoException>(() => map.Interpolate(-1, 1), "Interpolation failed."); + AssertException<VectoException>(() => map.Interpolate(1, 1), "Interpolation failed. x: 1, y: 1"); + AssertException<VectoException>(() => map.Interpolate(-1, -1), "Interpolation failed. x: -1, y: -1"); + AssertException<VectoException>(() => map.Interpolate(1, -1), "Interpolation failed. x: 1, y: -1"); + AssertException<VectoException>(() => map.Interpolate(-1, 1), "Interpolation failed. x: -1, y: 1"); }