PageRenderTime 46ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/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
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using Delta.Utilities.Datatypes;
  5. using Delta.Utilities.Helpers;
  6. using NUnit.Framework;
  7. namespace Delta.Utilities.Math
  8. {
  9. /// <summary>
  10. /// The graph is a list of points (X, Y) which are interpolated linearly
  11. /// when accessing a point between two existing points.
  12. /// </summary>
  13. public class Graph : ISaveLoadBinary
  14. {
  15. #region FromString (Static)
  16. /// <summary>
  17. /// From string
  18. /// </summary>
  19. public static Graph FromString(string graphString)
  20. {
  21. #region Validation
  22. if (String.IsNullOrEmpty(graphString))
  23. {
  24. // Return an empty graph to indicate the loading error
  25. return new Graph(Range.Zero).Add(0, 0);
  26. }
  27. #endregion
  28. // Split input string
  29. string[] splitted = graphString.SplitAndTrim(";");
  30. #region Validation
  31. if (splitted.Length < 2 &&
  32. splitted.Length % 2 != 0)
  33. {
  34. Log.Warning("Invalid string token length: " + splitted.Length);
  35. // Return an empty graph to indicate the loading error
  36. return new Graph(Range.Zero).Add(0, 0);
  37. }
  38. #endregion
  39. // Extract min and max x
  40. float minX = splitted[0].FromInvariantString(0.0f);
  41. float maxX = splitted[1].FromInvariantString(0.0f);
  42. // Create graph and extract data points
  43. Graph result = new Graph(new Range(minX, maxX));
  44. for (int index = 2; index < splitted.Length; index += 2)
  45. {
  46. // parse the x offset
  47. float offset = splitted[index].FromInvariantString(0.0f);
  48. // and the y value
  49. float value = splitted[index + 1].FromInvariantString(0.0f);
  50. // Add it to the graph
  51. result.Add(offset, value);
  52. }
  53. return result;
  54. }
  55. #endregion
  56. #region GraphRange (Public)
  57. /// <summary>
  58. /// The range in which our graph is used.
  59. /// </summary>
  60. public Range GraphRange;
  61. #endregion
  62. #region Private
  63. #region Points (Private)
  64. /// <summary>
  65. /// List of points which is sorted by the x values.
  66. /// </summary>
  67. private readonly SortedList<float, float> Points;
  68. #endregion
  69. #region cachedSegments (Private)
  70. /// <summary>
  71. /// Cached segments, which prevents recreation of new segments
  72. /// each time we want a new value.
  73. /// </summary>
  74. private LinearSegment[] cachedSegments;
  75. #endregion
  76. #endregion
  77. #region Constructors
  78. /// <summary>
  79. /// Create a new graph with the default length of 0-1.
  80. /// </summary>
  81. public Graph()
  82. {
  83. GraphRange = Range.ZeroToOne;
  84. Points = new SortedList<float, float>();
  85. cachedSegments = new LinearSegment[0];
  86. }
  87. /// <summary>
  88. /// Create graph with a total x-axis length of setStartEndRange.
  89. /// </summary>
  90. public Graph(Range setStartEndRange)
  91. : this()
  92. {
  93. GraphRange = setStartEndRange;
  94. }
  95. /// <summary>
  96. /// Create a new graph with a given y-axis start value with the default
  97. /// length of 0-1.
  98. /// </summary>
  99. public Graph(float yStartValue)
  100. : this()
  101. {
  102. Add(0, yStartValue);
  103. }
  104. #endregion
  105. #region ISaveLoadBinary Members
  106. /// <summary>
  107. /// Load the graph via the binary reader.
  108. /// </summary>
  109. /// <param name="reader">Reader for loading.</param>
  110. public void Load(BinaryReader reader)
  111. {
  112. GraphRange.Load(reader);
  113. int numberOfKeys = reader.ReadInt32();
  114. for (int index = 0; index < numberOfKeys; index++)
  115. {
  116. Add(reader.ReadSingle(), reader.ReadSingle());
  117. }
  118. }
  119. /// <summary>
  120. /// Save the graph via the binary writer.
  121. /// </summary>
  122. /// <param name="writer">Writer for saving.</param>
  123. public void Save(BinaryWriter writer)
  124. {
  125. GraphRange.Save(writer);
  126. writer.Write(Points.Count);
  127. foreach (KeyValuePair<float, float> pair in Points)
  128. {
  129. writer.Write(pair.Key);
  130. writer.Write(pair.Value);
  131. }
  132. }
  133. #endregion
  134. #region GetIntegratedValue (Public)
  135. /// <summary>
  136. /// Get integrated value with given value offset
  137. /// (add valueOffset to Y value)
  138. /// This returns the area below the curve up to given offset.
  139. /// Note: Only tested to work with graphs from 0 to 1 (x axis)
  140. /// </summary>
  141. public float GetIntegratedValue(float atOffset, float valueOffset)
  142. {
  143. bool exitAfterSegment = false;
  144. float result = 0;
  145. for (int index = 0; index < cachedSegments.Length; index++)
  146. {
  147. LinearSegment segment = GetSegment(index);
  148. float startX = segment.Start.X;
  149. float endX = segment.End.X;
  150. if (atOffset <= endX)
  151. {
  152. endX = atOffset;
  153. exitAfterSegment = true;
  154. }
  155. float startValue = segment.Start.Y + valueOffset;
  156. float endValue = segment.GetValue(endX) + valueOffset;
  157. float zeroOffset = float.NaN;
  158. if (startValue != endValue)
  159. {
  160. // Calculate zero offset (by calculating the linear equation,
  161. // then solving to zero)
  162. float incline = (endValue - startValue) / (endX - startX);
  163. float axisOffset = startValue - (startX * incline);
  164. // get zero point: f(x) = incline * x + axisOffset
  165. // == -axisOffset / incline
  166. float zeroPoint = -axisOffset / incline;
  167. // Now check if Zero point is inside start and end:
  168. if (zeroPoint > startX &&
  169. zeroPoint < endX)
  170. {
  171. zeroOffset = zeroPoint;
  172. }
  173. }
  174. if (float.IsNaN(zeroOffset))
  175. {
  176. // We do not have a zero point, so just integrate
  177. result += Integrate(startX, startValue,
  178. endX, endValue);
  179. }
  180. else
  181. {
  182. // We do have a zero point, so separately calculate for
  183. // upper and lower segment
  184. result += Integrate(startX, startValue,
  185. zeroOffset, 0);
  186. result += Integrate(zeroOffset, 0,
  187. endX, endValue);
  188. }
  189. if (exitAfterSegment)
  190. {
  191. break;
  192. }
  193. }
  194. return result;
  195. }
  196. /// <summary>
  197. /// This returns the area below the curve up to given offset.
  198. /// Note: Only tested to work with graphs from 0 to 1 (x axis),
  199. /// </summary>
  200. public float GetIntegratedValue(float atOffset)
  201. {
  202. return GetIntegratedValue(atOffset, 0.0f);
  203. }
  204. #endregion
  205. #region GetValue (Public)
  206. /// <summary>
  207. /// Get the value at the specified x value.
  208. /// </summary>
  209. public float GetValue(float atValue)
  210. {
  211. int segmentIndex = 0;
  212. // Find the current segment
  213. for (int i = 0; i < Points.Count; i++)
  214. {
  215. if (atValue >= Points.Keys[i])
  216. {
  217. segmentIndex = i;
  218. }
  219. else
  220. {
  221. return GetSegment(segmentIndex).GetValue(atValue);
  222. }
  223. }
  224. // just return the last segment if the requested value is beyond all
  225. // segments.
  226. return GetSegment(Points.Count - 2).GetValue(atValue);
  227. }
  228. #endregion
  229. #region Add (Public)
  230. /// <summary>
  231. /// Add a segment at the specified x value with the y 'height'.
  232. /// </summary>
  233. public Graph Add(float xValue, float yValue)
  234. {
  235. xValue = MathHelper.Clamp(xValue, GraphRange.Start, GraphRange.End);
  236. // we alter the existing point if we try to add another point at the
  237. // exact same place.
  238. int keyIndex = Points.Keys.IndexOf(xValue);
  239. if (keyIndex >= 0)
  240. {
  241. Points[xValue] = yValue;
  242. }
  243. else
  244. {
  245. Points.Add(xValue, yValue);
  246. }
  247. // Update the size of the segment cache.
  248. cachedSegments = new LinearSegment[Points.Count];
  249. return this;
  250. }
  251. #endregion
  252. #region ToString (Public)
  253. /// <summary>
  254. /// To string, will output everything with ';' separated strings.
  255. /// </summary>
  256. public override string ToString()
  257. {
  258. string result =
  259. GraphRange.Start.ToInvariantString() + ";" +
  260. GraphRange.End.ToInvariantString() + ";";
  261. for (int index = 0; index < Points.Count; index++)
  262. {
  263. result +=
  264. Points.Keys[index].ToInvariantString() + ";" +
  265. Points.Values[index].ToInvariantString() + ";";
  266. }
  267. return result;
  268. }
  269. #endregion
  270. #region Methods (Private)
  271. #region Integrate
  272. /// <summary>
  273. /// Calculates area covered between start and end and x-axis.
  274. /// Note: This method expects start and end Y to NOT cross the X axis.
  275. /// (Both startY and endY need to have the same sign).
  276. /// </summary>
  277. private float Integrate(float startX, float startY, float endX, float endY)
  278. {
  279. bool isNegative = false;
  280. if (startY < 0 ||
  281. endY < 0)
  282. {
  283. isNegative = true;
  284. // move values into positive area, we just negate at the end.
  285. startY = -startY;
  286. endY = -endY;
  287. }
  288. // Swap highest point to always be the second one
  289. if (endY < startY)
  290. {
  291. float temp = endY;
  292. endY = startY;
  293. startY = temp;
  294. }
  295. // Calculate area of rectangle and triangle, which is formed by the line
  296. float rectangleArea = (endX - startX) * (startY);
  297. float triangleArea = (endX - startX) * (endY - startY) / 2.0f;
  298. // Add them together for final area
  299. float area = rectangleArea + triangleArea;
  300. // Now return negative or positive based on input values
  301. return
  302. isNegative
  303. ? -area
  304. : area;
  305. }
  306. #endregion
  307. #region GetSegment
  308. /// <summary>
  309. /// Get the segment at the index.
  310. /// </summary>
  311. private LinearSegment GetSegment(int index)
  312. {
  313. if (index >= cachedSegments.Length)
  314. {
  315. return LinearSegment.Zero;
  316. }
  317. if (cachedSegments[index] == LinearSegment.Zero)
  318. {
  319. LinearSegment segment = new LinearSegment(
  320. new Point(Points.Keys[index], Points.Values[index]),
  321. new Point(Points.Keys[index + 1], Points.Values[index + 1]));
  322. return cachedSegments[index] = segment;
  323. }
  324. return cachedSegments[index];
  325. }
  326. #endregion
  327. #endregion
  328. /// <summary>
  329. /// Unit tests for the Graph class.
  330. /// </summary>
  331. internal class GraphTests
  332. {
  333. #region TestGraphLengthConstructor (LongRunning)
  334. /// <summary>
  335. /// Test the graph with the length constructor.
  336. /// </summary>
  337. [Test, Category("LongRunning")]
  338. public void TestGraphLengthConstructor()
  339. {
  340. Graph graph = new Graph(new Range(0, 10)).Add(0, 0);
  341. graph.Add(5, 1);
  342. graph.Add(10, 0);
  343. Assert.Equal(graph.GetValue(0.0f), 0.0f);
  344. Assert.Equal(graph.GetValue(5.0f), 1.0f);
  345. Assert.Equal(graph.GetValue(7.5f), 0.5f);
  346. Assert.Equal(graph.GetValue(10.0f), 0.0f);
  347. Assert.Equal(graph.GetValue(10.5f), 0.0f);
  348. }
  349. #endregion
  350. #region Integrate (LongRunning)
  351. /// <summary>
  352. /// Test the Integrate method
  353. /// </summary>
  354. [Test, Category("LongRunning")]
  355. public void Integrate()
  356. {
  357. Graph graph = new Graph(new Range(0, 1));
  358. graph.Add(0.0f, 1.0f);
  359. graph.Add(1.0f, 1.0f);
  360. // Test simple graphs
  361. Assert.NearlyEqual(graph.GetIntegratedValue(1.0f, 0.0f), 1.0f);
  362. Assert.NearlyEqual(graph.GetIntegratedValue(0.5f, 0.0f), 0.5f);
  363. graph.Add(0.0f, 0.0f);
  364. Assert.NearlyEqual(graph.GetIntegratedValue(1.0f, 0.0f), 0.5f);
  365. graph.Add(0.0f, 0.5f);
  366. Assert.NearlyEqual(graph.GetIntegratedValue(1.0f, 0.0f), 0.75f);
  367. graph = new Graph(new Range(0, 1));
  368. graph.Add(0.0f, -1.0f);
  369. graph.Add(1.0f, 1.0f);
  370. Assert.NearlyEqual(graph.GetIntegratedValue(1.0f, 0.0f), 0.0f);
  371. graph = new Graph(new Range(0, 1));
  372. graph.Add(0.0f, 0.0f);
  373. graph.Add(1.0f, 1.0f);
  374. Assert.NearlyEqual(graph.GetIntegratedValue(1.0f, -0.5f), 0.0f);
  375. // 1/8 of whole area
  376. Assert.NearlyEqual(graph.GetIntegratedValue(0.5f, -0.5f), -0.125f);
  377. // 1/16
  378. Assert.NearlyEqual(graph.GetIntegratedValue(0.75f, -0.5f), -0, 0625f);
  379. }
  380. #endregion
  381. #region TestSaveAndLoad (LongRunning)
  382. /// <summary>
  383. /// Test save and load functionality of the Graph class
  384. /// </summary>
  385. [Test, Category("LongRunning")]
  386. public void TestSaveAndLoad()
  387. {
  388. // Creation of the graph
  389. Graph graph = new Graph(new Range(0, 5)).Add(0, 1);
  390. graph.Add(1, 2);
  391. graph.Add(5, 0.5f);
  392. // Saving
  393. MemoryStream savedStream = new MemoryStream();
  394. BinaryWriter writer = new BinaryWriter(savedStream);
  395. graph.Save(writer);
  396. writer.Flush();
  397. writer = null;
  398. graph = null;
  399. // Loading
  400. savedStream.Position = 0;
  401. BinaryReader reader = new BinaryReader(savedStream);
  402. Graph loadedGraph = new Graph();
  403. loadedGraph.Load(reader);
  404. // Testing
  405. Assert.Equal(loadedGraph.GraphRange.Start, 0);
  406. Assert.Equal(loadedGraph.GraphRange.End, 5);
  407. Assert.Equal(loadedGraph.GetValue(0), 1);
  408. Assert.Equal(loadedGraph.GetValue(1), 2);
  409. Assert.Equal(loadedGraph.GetValue(5), 0.5f);
  410. }
  411. #endregion
  412. #region TestToStringFromString (LongRunning)
  413. /// <summary>
  414. /// Test ToString and FromString functionality of the Graph class
  415. /// </summary>
  416. [Test, Category("LongRunning")]
  417. public void TestToStringFromString()
  418. {
  419. // Creation of the graph
  420. Graph graph = new Graph(new Range(0, 5)).Add(0, 1);
  421. graph.Add(1, 2);
  422. graph.Add(5, 0.5f);
  423. // Saving
  424. string savedGraph = graph.ToString();
  425. // Loading
  426. Graph loadedGraph = FromString(savedGraph);
  427. // Testing
  428. Assert.Equal(loadedGraph.GraphRange.Start, 0);
  429. Assert.Equal(loadedGraph.GraphRange.End, 5);
  430. Assert.Equal(loadedGraph.GetValue(0), 1);
  431. Assert.Equal(loadedGraph.GetValue(1), 2);
  432. Assert.Equal(loadedGraph.GetValue(5), 0.5f);
  433. }
  434. #endregion
  435. }
  436. }
  437. }