#### /Utilities/Math/Graph.cs

#
C# | 484 lines | 322 code | 47 blank | 115 comment | 21 complexity | c47204f0646d7214007006599e0d038c MD5 | raw file
``````
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>

{

#region FromString (Static)

/// <summary>

/// From string

/// </summary>

public static Graph FromString(string graphString)

{

#region Validation

if (String.IsNullOrEmpty(graphString))

{

}

#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);

}

#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

}

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>

#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()

{

}

#endregion

/// <summary>

/// </summary>

{

for (int index = 0; index < numberOfKeys; index++)

{

}

}

/// <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

/// <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

{

}

// 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);

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));

// Test simple graphs

Assert.NearlyEqual(graph.GetIntegratedValue(1.0f, 0.0f), 1.0f);

Assert.NearlyEqual(graph.GetIntegratedValue(0.5f, 0.0f), 0.5f);

Assert.NearlyEqual(graph.GetIntegratedValue(1.0f, 0.0f), 0.5f);

Assert.NearlyEqual(graph.GetIntegratedValue(1.0f, 0.0f), 0.75f);

graph = new Graph(new Range(0, 1));

Assert.NearlyEqual(graph.GetIntegratedValue(1.0f, 0.0f), 0.0f);

graph = new Graph(new Range(0, 1));

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

/// <summary>

/// Test save and load functionality of the Graph class

/// </summary>

[Test, Category("LongRunning")]

{

// Creation of the graph

Graph graph = new Graph(new Range(0, 5)).Add(0, 1);

// Saving

MemoryStream savedStream = new MemoryStream();

BinaryWriter writer = new BinaryWriter(savedStream);

graph.Save(writer);

writer.Flush();

writer = null;

graph = null;

savedStream.Position = 0;

// Testing

}

#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);

// Saving

string savedGraph = graph.ToString();

// Testing

}

#endregion

}

}

}

``````