/Utilities/Math/Graph.cs
C# | 484 lines | 322 code | 47 blank | 115 comment | 21 complexity | c47204f0646d7214007006599e0d038c MD5 | raw file
Possible License(s): Apache-2.0
- using System;
- using System.Collections.Generic;
- using System.IO;
- using Delta.Utilities.Datatypes;
- using Delta.Utilities.Helpers;
- using NUnit.Framework;
-
- namespace Delta.Utilities.Math
- {
- /// <summary>
- /// The graph is a list of points (X, Y) which are interpolated linearly
- /// when accessing a point between two existing points.
- /// </summary>
- public class Graph : ISaveLoadBinary
- {
- #region FromString (Static)
- /// <summary>
- /// From string
- /// </summary>
- public static Graph FromString(string graphString)
- {
- #region Validation
- if (String.IsNullOrEmpty(graphString))
- {
- // Return an empty graph to indicate the loading error
- return new Graph(Range.Zero).Add(0, 0);
- }
- #endregion
-
- // Split input string
- string[] splitted = graphString.SplitAndTrim(";");
-
- #region Validation
- if (splitted.Length < 2 &&
- splitted.Length % 2 != 0)
- {
- Log.Warning("Invalid string token length: " + splitted.Length);
- // Return an empty graph to indicate the loading error
- return new Graph(Range.Zero).Add(0, 0);
- }
- #endregion
-
- // Extract min and max x
- float minX = splitted[0].FromInvariantString(0.0f);
- float maxX = splitted[1].FromInvariantString(0.0f);
-
- // Create graph and extract data points
- Graph result = new Graph(new Range(minX, maxX));
- for (int index = 2; index < splitted.Length; index += 2)
- {
- // parse the x offset
- float offset = splitted[index].FromInvariantString(0.0f);
- // and the y value
- float value = splitted[index + 1].FromInvariantString(0.0f);
- // Add it to the graph
- result.Add(offset, value);
- }
- return result;
- }
- #endregion
-
- #region GraphRange (Public)
- /// <summary>
- /// The range in which our graph is used.
- /// </summary>
- public Range GraphRange;
- #endregion
-
- #region Private
-
- #region Points (Private)
- /// <summary>
- /// List of points which is sorted by the x values.
- /// </summary>
- private readonly SortedList<float, float> Points;
- #endregion
-
- #region cachedSegments (Private)
- /// <summary>
- /// Cached segments, which prevents recreation of new segments
- /// each time we want a new value.
- /// </summary>
- private LinearSegment[] cachedSegments;
- #endregion
-
- #endregion
-
- #region Constructors
- /// <summary>
- /// Create a new graph with the default length of 0-1.
- /// </summary>
- public Graph()
- {
- GraphRange = Range.ZeroToOne;
- Points = new SortedList<float, float>();
- cachedSegments = new LinearSegment[0];
- }
-
- /// <summary>
- /// Create graph with a total x-axis length of setStartEndRange.
- /// </summary>
- public Graph(Range setStartEndRange)
- : this()
- {
- GraphRange = setStartEndRange;
- }
-
- /// <summary>
- /// Create a new graph with a given y-axis start value with the default
- /// length of 0-1.
- /// </summary>
- public Graph(float yStartValue)
- : this()
- {
- Add(0, yStartValue);
- }
- #endregion
-
- #region ISaveLoadBinary Members
- /// <summary>
- /// Load the graph via the binary reader.
- /// </summary>
- /// <param name="reader">Reader for loading.</param>
- public void Load(BinaryReader reader)
- {
- GraphRange.Load(reader);
- int numberOfKeys = reader.ReadInt32();
- for (int index = 0; index < numberOfKeys; index++)
- {
- Add(reader.ReadSingle(), reader.ReadSingle());
- }
- }
-
- /// <summary>
- /// Save the graph via the binary writer.
- /// </summary>
- /// <param name="writer">Writer for saving.</param>
- public void Save(BinaryWriter writer)
- {
- GraphRange.Save(writer);
- writer.Write(Points.Count);
- foreach (KeyValuePair<float, float> pair in Points)
- {
- writer.Write(pair.Key);
- writer.Write(pair.Value);
- }
- }
- #endregion
-
- #region GetIntegratedValue (Public)
- /// <summary>
- /// Get integrated value with given value offset
- /// (add valueOffset to Y value)
- /// This returns the area below the curve up to given offset.
- /// Note: Only tested to work with graphs from 0 to 1 (x axis)
- /// </summary>
- public float GetIntegratedValue(float atOffset, float valueOffset)
- {
- bool exitAfterSegment = false;
- float result = 0;
- for (int index = 0; index < cachedSegments.Length; index++)
- {
- LinearSegment segment = GetSegment(index);
- float startX = segment.Start.X;
- float endX = segment.End.X;
- if (atOffset <= endX)
- {
- endX = atOffset;
- exitAfterSegment = true;
- }
- float startValue = segment.Start.Y + valueOffset;
- float endValue = segment.GetValue(endX) + valueOffset;
-
- float zeroOffset = float.NaN;
- if (startValue != endValue)
- {
- // Calculate zero offset (by calculating the linear equation,
- // then solving to zero)
- float incline = (endValue - startValue) / (endX - startX);
- float axisOffset = startValue - (startX * incline);
- // get zero point: f(x) = incline * x + axisOffset
- // == -axisOffset / incline
- float zeroPoint = -axisOffset / incline;
- // Now check if Zero point is inside start and end:
- if (zeroPoint > startX &&
- zeroPoint < endX)
- {
- zeroOffset = zeroPoint;
- }
- }
- if (float.IsNaN(zeroOffset))
- {
- // We do not have a zero point, so just integrate
- result += Integrate(startX, startValue,
- endX, endValue);
- }
- else
- {
- // We do have a zero point, so separately calculate for
- // upper and lower segment
- result += Integrate(startX, startValue,
- zeroOffset, 0);
- result += Integrate(zeroOffset, 0,
- endX, endValue);
- }
- if (exitAfterSegment)
- {
- break;
- }
- }
- return result;
- }
-
- /// <summary>
- /// This returns the area below the curve up to given offset.
- /// Note: Only tested to work with graphs from 0 to 1 (x axis),
- /// </summary>
- public float GetIntegratedValue(float atOffset)
- {
- return GetIntegratedValue(atOffset, 0.0f);
- }
- #endregion
-
- #region GetValue (Public)
- /// <summary>
- /// Get the value at the specified x value.
- /// </summary>
- public float GetValue(float atValue)
- {
- int segmentIndex = 0;
-
- // Find the current segment
- for (int i = 0; i < Points.Count; i++)
- {
- if (atValue >= Points.Keys[i])
- {
- segmentIndex = i;
- }
- else
- {
- return GetSegment(segmentIndex).GetValue(atValue);
- }
- }
-
- // just return the last segment if the requested value is beyond all
- // segments.
- return GetSegment(Points.Count - 2).GetValue(atValue);
- }
- #endregion
-
- #region Add (Public)
- /// <summary>
- /// Add a segment at the specified x value with the y 'height'.
- /// </summary>
- public Graph Add(float xValue, float yValue)
- {
- xValue = MathHelper.Clamp(xValue, GraphRange.Start, GraphRange.End);
-
- // we alter the existing point if we try to add another point at the
- // exact same place.
- int keyIndex = Points.Keys.IndexOf(xValue);
- if (keyIndex >= 0)
- {
- Points[xValue] = yValue;
- }
- else
- {
- Points.Add(xValue, yValue);
- }
-
- // Update the size of the segment cache.
- cachedSegments = new LinearSegment[Points.Count];
- return this;
- }
- #endregion
-
- #region ToString (Public)
- /// <summary>
- /// To string, will output everything with ';' separated strings.
- /// </summary>
- public override string ToString()
- {
- string result =
- GraphRange.Start.ToInvariantString() + ";" +
- GraphRange.End.ToInvariantString() + ";";
-
- for (int index = 0; index < Points.Count; index++)
- {
- result +=
- Points.Keys[index].ToInvariantString() + ";" +
- Points.Values[index].ToInvariantString() + ";";
- }
- return result;
- }
- #endregion
-
- #region Methods (Private)
-
- #region Integrate
- /// <summary>
- /// Calculates area covered between start and end and x-axis.
- /// Note: This method expects start and end Y to NOT cross the X axis.
- /// (Both startY and endY need to have the same sign).
- /// </summary>
- private float Integrate(float startX, float startY, float endX, float endY)
- {
- bool isNegative = false;
- if (startY < 0 ||
- endY < 0)
- {
- isNegative = true;
- // move values into positive area, we just negate at the end.
- startY = -startY;
- endY = -endY;
- }
- // Swap highest point to always be the second one
- if (endY < startY)
- {
- float temp = endY;
- endY = startY;
- startY = temp;
- }
- // Calculate area of rectangle and triangle, which is formed by the line
- float rectangleArea = (endX - startX) * (startY);
- float triangleArea = (endX - startX) * (endY - startY) / 2.0f;
- // Add them together for final area
- float area = rectangleArea + triangleArea;
- // Now return negative or positive based on input values
- return
- isNegative
- ? -area
- : area;
- }
- #endregion
-
- #region GetSegment
- /// <summary>
- /// Get the segment at the index.
- /// </summary>
- private LinearSegment GetSegment(int index)
- {
- if (index >= cachedSegments.Length)
- {
- return LinearSegment.Zero;
- }
- if (cachedSegments[index] == LinearSegment.Zero)
- {
- LinearSegment segment = new LinearSegment(
- new Point(Points.Keys[index], Points.Values[index]),
- new Point(Points.Keys[index + 1], Points.Values[index + 1]));
-
- return cachedSegments[index] = segment;
- }
-
- return cachedSegments[index];
- }
- #endregion
-
- #endregion
-
- /// <summary>
- /// Unit tests for the Graph class.
- /// </summary>
- internal class GraphTests
- {
- #region TestGraphLengthConstructor (LongRunning)
- /// <summary>
- /// Test the graph with the length constructor.
- /// </summary>
- [Test, Category("LongRunning")]
- public void TestGraphLengthConstructor()
- {
- Graph graph = new Graph(new Range(0, 10)).Add(0, 0);
- graph.Add(5, 1);
- graph.Add(10, 0);
-
- Assert.Equal(graph.GetValue(0.0f), 0.0f);
- Assert.Equal(graph.GetValue(5.0f), 1.0f);
- Assert.Equal(graph.GetValue(7.5f), 0.5f);
- Assert.Equal(graph.GetValue(10.0f), 0.0f);
- Assert.Equal(graph.GetValue(10.5f), 0.0f);
- }
- #endregion
-
- #region Integrate (LongRunning)
- /// <summary>
- /// Test the Integrate method
- /// </summary>
- [Test, Category("LongRunning")]
- public void Integrate()
- {
- Graph graph = new Graph(new Range(0, 1));
- graph.Add(0.0f, 1.0f);
- graph.Add(1.0f, 1.0f);
-
- // Test simple graphs
- Assert.NearlyEqual(graph.GetIntegratedValue(1.0f, 0.0f), 1.0f);
- Assert.NearlyEqual(graph.GetIntegratedValue(0.5f, 0.0f), 0.5f);
- graph.Add(0.0f, 0.0f);
- Assert.NearlyEqual(graph.GetIntegratedValue(1.0f, 0.0f), 0.5f);
- graph.Add(0.0f, 0.5f);
- Assert.NearlyEqual(graph.GetIntegratedValue(1.0f, 0.0f), 0.75f);
-
- graph = new Graph(new Range(0, 1));
- graph.Add(0.0f, -1.0f);
- graph.Add(1.0f, 1.0f);
- Assert.NearlyEqual(graph.GetIntegratedValue(1.0f, 0.0f), 0.0f);
-
- graph = new Graph(new Range(0, 1));
- graph.Add(0.0f, 0.0f);
- graph.Add(1.0f, 1.0f);
- Assert.NearlyEqual(graph.GetIntegratedValue(1.0f, -0.5f), 0.0f);
- // 1/8 of whole area
- Assert.NearlyEqual(graph.GetIntegratedValue(0.5f, -0.5f), -0.125f);
- // 1/16
- Assert.NearlyEqual(graph.GetIntegratedValue(0.75f, -0.5f), -0, 0625f);
- }
- #endregion
-
- #region TestSaveAndLoad (LongRunning)
- /// <summary>
- /// Test save and load functionality of the Graph class
- /// </summary>
- [Test, Category("LongRunning")]
- public void TestSaveAndLoad()
- {
- // Creation of the graph
- Graph graph = new Graph(new Range(0, 5)).Add(0, 1);
- graph.Add(1, 2);
- graph.Add(5, 0.5f);
-
- // Saving
- MemoryStream savedStream = new MemoryStream();
- BinaryWriter writer = new BinaryWriter(savedStream);
- graph.Save(writer);
- writer.Flush();
- writer = null;
- graph = null;
-
- // Loading
- savedStream.Position = 0;
- BinaryReader reader = new BinaryReader(savedStream);
- Graph loadedGraph = new Graph();
- loadedGraph.Load(reader);
-
- // Testing
- Assert.Equal(loadedGraph.GraphRange.Start, 0);
- Assert.Equal(loadedGraph.GraphRange.End, 5);
- Assert.Equal(loadedGraph.GetValue(0), 1);
- Assert.Equal(loadedGraph.GetValue(1), 2);
- Assert.Equal(loadedGraph.GetValue(5), 0.5f);
- }
- #endregion
-
- #region TestToStringFromString (LongRunning)
- /// <summary>
- /// Test ToString and FromString functionality of the Graph class
- /// </summary>
- [Test, Category("LongRunning")]
- public void TestToStringFromString()
- {
- // Creation of the graph
- Graph graph = new Graph(new Range(0, 5)).Add(0, 1);
- graph.Add(1, 2);
- graph.Add(5, 0.5f);
-
- // Saving
- string savedGraph = graph.ToString();
-
- // Loading
- Graph loadedGraph = FromString(savedGraph);
-
- // Testing
- Assert.Equal(loadedGraph.GraphRange.Start, 0);
- Assert.Equal(loadedGraph.GraphRange.End, 5);
- Assert.Equal(loadedGraph.GetValue(0), 1);
- Assert.Equal(loadedGraph.GetValue(1), 2);
- Assert.Equal(loadedGraph.GetValue(5), 0.5f);
- }
- #endregion
- }
- }
- }
-