From 52390d58e4201e4a366670a924dc9934af7e753e Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 17 Nov 2023 19:18:40 +0100 Subject: [PATCH 01/34] implement bezier approximation algorithm --- osu.Framework/Utils/PathApproximator.cs | 156 ++++++++++++++++++++++++ osu.Framework/osu.Framework.csproj | 1 + 2 files changed, 157 insertions(+) diff --git a/osu.Framework/Utils/PathApproximator.cs b/osu.Framework/Utils/PathApproximator.cs index 887572007a..fa94d214e8 100644 --- a/osu.Framework/Utils/PathApproximator.cs +++ b/osu.Framework/Utils/PathApproximator.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Framework.Graphics.Primitives; using osuTK; +using NumSharp; namespace osu.Framework.Utils { @@ -305,6 +306,161 @@ public static List LagrangePolynomialToPiecewiseLinear(ReadOnlySpan PiecewiseLinearToBezier(ReadOnlySpan inputPath, int numControlPoints, int numTestPoints = 200, int maxIterations = 100) + { + const float learning_rate = 8f; + const float b1 = 0.9f; + const float b2 = 0.92f; + const float epsilon = 1E-8f; + + // Generate Bezier weight matrix + var weights = generateBezierWeights(numControlPoints, numTestPoints); + var weightsTranspose = weights.T; + + // Create efficient interpolation on the input path + var interpolator = new Interpolator(inputPath); + + // Create initial control point positions equally spaced along the input path + var controlPoints = interpolator.Interpolate(np.linspace(0, 1, numControlPoints)); + NDArray labels = null!; + + // Initialize Adam optimizer variables + var m = np.zeros_like(controlPoints); + var v = np.zeros_like(controlPoints); + var learnableMask = np.zeros_like(controlPoints); + learnableMask["1:-1"] = 1; + + for (int step = 0; step < maxIterations; step++) + { + var points = np.matmul(weights, controlPoints); + + // Update labels to shift the distance distribution between points + if (step % 11 == 0) + labels = interpolateWithDistanceDistribution(points, interpolator); + + // Calculate the gradient on the control points + var diff = labels - points; + var grad = -1 / numControlPoints * np.matmul(weightsTranspose, diff); + + // Apply learnable mask to prevent moving the endpoints + grad *= learnableMask; + + // Update control points with Adam optimizer + m = b1 * m + (1 - b1) * grad; + v = b2 * v + (1 - b2) * np.square(grad); + var mCorr = m / (1 - Math.Pow(b1, step)); + var vCorr = v / (1 - Math.Pow(b2, step)); + controlPoints -= learning_rate * mCorr / (np.sqrt(vCorr) + epsilon); + } + + // Convert the resulting control points NDArray + var result = new List(numControlPoints); + + for (int i = 0; i < numControlPoints; i++) + { + result.Add(new Vector2(controlPoints[i, 0], controlPoints[i, 1])); + } + + return result; + } + + private static NDArray getDistanceDistribution(NDArray points) + { + var distCumulativeSum = np.cumsum(np.sqrt(np.sum(np.square(points["1:"] - points[":-1"]), -1))); + distCumulativeSum = np.concatenate(new[] { np.zeros(1), distCumulativeSum }); + return distCumulativeSum / distCumulativeSum[-1]; + } + + private static NDArray interpolateWithDistanceDistribution(NDArray points, Interpolator interpolator) + { + return interpolator.Interpolate(getDistanceDistribution(points)); + } + + private class Interpolator + { + private readonly int ny; + private readonly NDArray ys; + + public Interpolator(ReadOnlySpan inputPath, int resolution = 1000) + { + var arr = np.array(inputPath.ToArray()); + var dist = getDistanceDistribution(arr); + ny = resolution; + ys = np.empty(resolution, arr.shape[1]); + int current = 0; + + for (int i = 0; i < resolution; i++) + { + float target = (float)i / (resolution - 1); + + while (dist[current] < target) + current++; + + int prev = Math.Max(0, current - 1); + float t = (dist[current] - target) / (dist[current] - dist[prev]); + + if (float.IsNaN(t)) + t = 0; + + ys[i] = t * arr[prev] + (1 - t) * arr[current]; + } + } + + public NDArray Interpolate(NDArray x) + { + var xIdx = x * (ny - 1); + var idxBelow = np.floor(xIdx); + var idxAbove = np.minimum(idxBelow + 1, ny - 1); + idxBelow = np.maximum(idxAbove - 1, 0); + + var yRefBelow = ys[idxBelow]; + var yRefAbove = ys[idxAbove]; + + var t = xIdx - idxBelow; + + var y = t * yRefAbove + (1 - t) * yRefBelow; + return y; + } + } + + private static NDArray generateBezierWeights(int numControlPoints, int numTestPoints) + { + var ts = np.linspace(0, 1, numTestPoints); + var coefficients = binomialCoefficients(numControlPoints); + var p = np.empty(numTestPoints, numControlPoints); + + for (int i = 0; i < numTestPoints; i++) + { + p[i, 0] = 1; + float t = ts[i]; + + for (int j = 1; j < numControlPoints; j++) + { + p[i, j] = p[i, j - 1] * t; + } + } + + return coefficients * p["::-1, ::-1"] * p; + } + + private static NDArray binomialCoefficients(int n) + { + var coefficients = np.empty(n); + coefficients[0] = 1; + + for (int i = 1; i < (n + 1) / 2; i++) + { + coefficients[i] = coefficients[i - 1] * (n - i + 1) / i; + } + + for (int i = n - 1; i > (n - 1) / 2; i--) + { + coefficients[i] = coefficients[n - i - 1]; + } + + return coefficients; + } + /// /// Make sure the 2nd order derivative (approximated using finite elements) is within tolerable bounds. /// NOTE: The 2nd order derivative of a 2d curve represents its curvature, so intuitively this function diff --git a/osu.Framework/osu.Framework.csproj b/osu.Framework/osu.Framework.csproj index 3ebc16d4f8..588e7ae589 100644 --- a/osu.Framework/osu.Framework.csproj +++ b/osu.Framework/osu.Framework.csproj @@ -24,6 +24,7 @@ + From e2c0d92f6a2809481e448a361ea83fed2c9303e3 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 18 Nov 2023 00:17:49 +0100 Subject: [PATCH 02/34] fixes and added test --- .../Drawables/TestSceneLinearToBezier.cs | 155 ++++++++++++++++++ osu.Framework/Utils/PathApproximator.cs | 34 ++-- 2 files changed, 176 insertions(+), 13 deletions(-) create mode 100644 osu.Framework.Tests/Visual/Drawables/TestSceneLinearToBezier.cs diff --git a/osu.Framework.Tests/Visual/Drawables/TestSceneLinearToBezier.cs b/osu.Framework.Tests/Visual/Drawables/TestSceneLinearToBezier.cs new file mode 100644 index 0000000000..03b7a6c93d --- /dev/null +++ b/osu.Framework.Tests/Visual/Drawables/TestSceneLinearToBezier.cs @@ -0,0 +1,155 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Lines; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Utils; +using osu.Framework.Testing; +using osuTK; +using osuTK.Graphics; + +namespace osu.Framework.Tests.Visual.Drawables +{ + public partial class TestSceneLinearToBezier : GridTestScene + { + private int numControlPoints = 5; + private int numTestPoints = 100; + private int maxIterations = 1000; + + private readonly List doubleTests = new List(); + + public TestSceneLinearToBezier() + : base(2, 2) + { + doubleTests.Add(new DoubleApproximatedPathTest(PathApproximator.BezierToPiecewiseLinear, numControlPoints, numTestPoints, maxIterations)); + Cell(0).AddRange(new[] + { + createLabel(nameof(PathApproximator.BezierToPiecewiseLinear)), + new ApproximatedPathTest(PathApproximator.BezierToPiecewiseLinear), + doubleTests[^1], + }); + + doubleTests.Add(new DoubleApproximatedPathTest(PathApproximator.CatmullToPiecewiseLinear, numControlPoints, numTestPoints, maxIterations)); + Cell(1).AddRange(new[] + { + createLabel(nameof(PathApproximator.CatmullToPiecewiseLinear)), + new ApproximatedPathTest(PathApproximator.CatmullToPiecewiseLinear), + doubleTests[^1], + }); + + doubleTests.Add(new DoubleApproximatedPathTest(PathApproximator.CircularArcToPiecewiseLinear, numControlPoints, numTestPoints, maxIterations)); + Cell(2).AddRange(new[] + { + createLabel(nameof(PathApproximator.CircularArcToPiecewiseLinear)), + new ApproximatedPathTest(PathApproximator.CircularArcToPiecewiseLinear), + doubleTests[^1], + }); + + doubleTests.Add(new DoubleApproximatedPathTest(PathApproximator.LagrangePolynomialToPiecewiseLinear, numControlPoints, numTestPoints, maxIterations)); + Cell(3).AddRange(new[] + { + createLabel(nameof(PathApproximator.LagrangePolynomialToPiecewiseLinear)), + new ApproximatedPathTest(PathApproximator.LagrangePolynomialToPiecewiseLinear), + doubleTests[^1], + }); + + AddSliderStep($"{nameof(numControlPoints)}", 3, 25, 5, v => + { + numControlPoints = v; + updateTests(); + }); + + AddSliderStep($"{nameof(numTestPoints)}", 10, 1000, 100, v => + { + numTestPoints = v; + updateTests(); + }); + + AddSliderStep($"{nameof(maxIterations)}", 0, 200, 10, v => + { + maxIterations = v; + updateTests(); + }); + } + + private void updateTests() + { + foreach (var test in doubleTests) + { + test.NumControlPoints = numControlPoints; + test.NumTestPoints = numTestPoints; + test.MaxIterations = maxIterations; + test.UpdatePath(); + } + } + + private Drawable createLabel(string text) => new SpriteText + { + Text = text, + Font = new FontUsage(size: 20), + Colour = Color4.White, + }; + + public delegate List ApproximatorFunc(ReadOnlySpan controlPoints); + + private partial class ApproximatedPathTest : SmoothPath + { + public ApproximatedPathTest(ApproximatorFunc approximator) + { + Vector2[] points = new Vector2[5]; + points[0] = new Vector2(50, 250); + points[1] = new Vector2(150, 230); + points[2] = new Vector2(100, 150); + points[3] = new Vector2(200, 80); + points[4] = new Vector2(250, 50); + + AutoSizeAxes = Axes.None; + RelativeSizeAxes = Axes.Both; + PathRadius = 2; + Vertices = approximator(points); + Colour = Color4.White; + } + } + + private partial class DoubleApproximatedPathTest : SmoothPath + { + private readonly Vector2[] inputPath; + + public int NumControlPoints { get; set; } + + public int NumTestPoints { get; set; } + + public int MaxIterations { get; set; } + + public DoubleApproximatedPathTest(ApproximatorFunc approximator, int numControlPoints, int numTestPoints, int maxIterations) + { + Vector2[] points = new Vector2[5]; + points[0] = new Vector2(50, 250); + points[1] = new Vector2(150, 230); + points[2] = new Vector2(100, 150); + points[3] = new Vector2(200, 80); + points[4] = new Vector2(250, 50); + + AutoSizeAxes = Axes.None; + RelativeSizeAxes = Axes.Both; + PathRadius = 2; + Colour = Color4.Magenta; + + NumControlPoints = numControlPoints; + NumTestPoints = numTestPoints; + MaxIterations = maxIterations; + inputPath = approximator(points).ToArray(); + UpdatePath(); + } + + public void UpdatePath() + { + var controlPoints = PathApproximator.PiecewiseLinearToBezier(inputPath, NumControlPoints, NumTestPoints, MaxIterations); + Vertices = PathApproximator.BezierToPiecewiseLinear(controlPoints.ToArray()); + } + } + } +} diff --git a/osu.Framework/Utils/PathApproximator.cs b/osu.Framework/Utils/PathApproximator.cs index fa94d214e8..d30d61632e 100644 --- a/osu.Framework/Utils/PathApproximator.cs +++ b/osu.Framework/Utils/PathApproximator.cs @@ -312,13 +312,14 @@ public static List PiecewiseLinearToBezier(ReadOnlySpan inputP const float b1 = 0.9f; const float b2 = 0.92f; const float epsilon = 1E-8f; + const int interpolator_resolution = 100; // Generate Bezier weight matrix var weights = generateBezierWeights(numControlPoints, numTestPoints); var weightsTranspose = weights.T; // Create efficient interpolation on the input path - var interpolator = new Interpolator(inputPath); + var interpolator = new Interpolator(inputPath, interpolator_resolution); // Create initial control point positions equally spaced along the input path var controlPoints = interpolator.Interpolate(np.linspace(0, 1, numControlPoints)); @@ -340,7 +341,7 @@ public static List PiecewiseLinearToBezier(ReadOnlySpan inputP // Calculate the gradient on the control points var diff = labels - points; - var grad = -1 / numControlPoints * np.matmul(weightsTranspose, diff); + var grad = -1f / numControlPoints * np.matmul(weightsTranspose, diff); // Apply learnable mask to prevent moving the endpoints grad *= learnableMask; @@ -348,8 +349,8 @@ public static List PiecewiseLinearToBezier(ReadOnlySpan inputP // Update control points with Adam optimizer m = b1 * m + (1 - b1) * grad; v = b2 * v + (1 - b2) * np.square(grad); - var mCorr = m / (1 - Math.Pow(b1, step)); - var vCorr = v / (1 - Math.Pow(b2, step)); + var mCorr = m / (1 - Math.Pow(b1, step + 1)); + var vCorr = v / (1 - Math.Pow(b2, step + 1)); controlPoints -= learning_rate * mCorr / (np.sqrt(vCorr) + epsilon); } @@ -366,8 +367,8 @@ public static List PiecewiseLinearToBezier(ReadOnlySpan inputP private static NDArray getDistanceDistribution(NDArray points) { - var distCumulativeSum = np.cumsum(np.sqrt(np.sum(np.square(points["1:"] - points[":-1"]), -1))); - distCumulativeSum = np.concatenate(new[] { np.zeros(1), distCumulativeSum }); + var distCumulativeSum = np.cumsum(np.sqrt(np.sum(np.square(points["1:"] - points[":-1"]), -1, NPTypeCode.Single)), typeCode: NPTypeCode.Single); + distCumulativeSum = np.concatenate(new[] { np.zeros(new Shape(1), NPTypeCode.Single), distCumulativeSum }); return distCumulativeSum / distCumulativeSum[-1]; } @@ -383,10 +384,17 @@ private class Interpolator public Interpolator(ReadOnlySpan inputPath, int resolution = 1000) { - var arr = np.array(inputPath.ToArray()); + var arr = np.empty(new Shape(inputPath.Length, 2), NPTypeCode.Single); + + for (int i = 0; i < inputPath.Length; i++) + { + arr[i, 0] = inputPath[i].X; + arr[i, 1] = inputPath[i].Y; + } + var dist = getDistanceDistribution(arr); ny = resolution; - ys = np.empty(resolution, arr.shape[1]); + ys = np.empty(new Shape(resolution, arr.shape[1]), NPTypeCode.Single); int current = 0; for (int i = 0; i < resolution; i++) @@ -418,7 +426,7 @@ public NDArray Interpolate(NDArray x) var t = xIdx - idxBelow; - var y = t * yRefAbove + (1 - t) * yRefBelow; + var y = t[Slice.All, np.newaxis] * yRefAbove + (1 - t)[Slice.All, np.newaxis] * yRefBelow; return y; } } @@ -426,8 +434,8 @@ public NDArray Interpolate(NDArray x) private static NDArray generateBezierWeights(int numControlPoints, int numTestPoints) { var ts = np.linspace(0, 1, numTestPoints); - var coefficients = binomialCoefficients(numControlPoints); - var p = np.empty(numTestPoints, numControlPoints); + var coefficients = binomialCoefficients(numControlPoints).astype(NPTypeCode.Single); + var p = np.empty(new Shape(numTestPoints, numControlPoints), NPTypeCode.Single); for (int i = 0; i < numTestPoints; i++) { @@ -445,12 +453,12 @@ private static NDArray generateBezierWeights(int numControlPoints, int numTestPo private static NDArray binomialCoefficients(int n) { - var coefficients = np.empty(n); + var coefficients = np.empty(new Shape(n), NPTypeCode.Int64); coefficients[0] = 1; for (int i = 1; i < (n + 1) / 2; i++) { - coefficients[i] = coefficients[i - 1] * (n - i + 1) / i; + coefficients[i] = coefficients[i - 1] * (n - i) / i; } for (int i = n - 1; i > (n - 1) / 2; i--) From 4133d6ea20078d1bf0ead026324f2e9cbc45ab9d Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 18 Nov 2023 00:29:19 +0100 Subject: [PATCH 03/34] enable optimization process later to improve loading time --- .../Visual/Drawables/TestSceneLinearToBezier.cs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/osu.Framework.Tests/Visual/Drawables/TestSceneLinearToBezier.cs b/osu.Framework.Tests/Visual/Drawables/TestSceneLinearToBezier.cs index 03b7a6c93d..79f78db727 100644 --- a/osu.Framework.Tests/Visual/Drawables/TestSceneLinearToBezier.cs +++ b/osu.Framework.Tests/Visual/Drawables/TestSceneLinearToBezier.cs @@ -15,9 +15,9 @@ namespace osu.Framework.Tests.Visual.Drawables { public partial class TestSceneLinearToBezier : GridTestScene { - private int numControlPoints = 5; - private int numTestPoints = 100; - private int maxIterations = 1000; + private int numControlPoints; + private int numTestPoints; + private int maxIterations; private readonly List doubleTests = new List(); @@ -73,6 +73,13 @@ public TestSceneLinearToBezier() maxIterations = v; updateTests(); }); + + AddStep("Enable optimization", () => + { + foreach (var test in doubleTests) + test.OptimizePath = true; + updateTests(); + }); } private void updateTests() @@ -124,6 +131,8 @@ private partial class DoubleApproximatedPathTest : SmoothPath public int MaxIterations { get; set; } + public bool OptimizePath { get; set; } + public DoubleApproximatedPathTest(ApproximatorFunc approximator, int numControlPoints, int numTestPoints, int maxIterations) { Vector2[] points = new Vector2[5]; @@ -147,6 +156,7 @@ public DoubleApproximatedPathTest(ApproximatorFunc approximator, int numControlP public void UpdatePath() { + if (!OptimizePath) return; var controlPoints = PathApproximator.PiecewiseLinearToBezier(inputPath, NumControlPoints, NumTestPoints, MaxIterations); Vertices = PathApproximator.BezierToPiecewiseLinear(controlPoints.ToArray()); } From 54e96a8e02fface15f034dfb604183b4b086b6dd Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 18 Nov 2023 13:26:39 +0100 Subject: [PATCH 04/34] clean up and rename stuff --- ...cs => TestScenePiecewiseLinearToBezier.cs} | 29 ++++++++++--------- osu.Framework/Utils/PathApproximator.cs | 7 +---- 2 files changed, 16 insertions(+), 20 deletions(-) rename osu.Framework.Tests/Visual/Drawables/{TestSceneLinearToBezier.cs => TestScenePiecewiseLinearToBezier.cs} (79%) diff --git a/osu.Framework.Tests/Visual/Drawables/TestSceneLinearToBezier.cs b/osu.Framework.Tests/Visual/Drawables/TestScenePiecewiseLinearToBezier.cs similarity index 79% rename from osu.Framework.Tests/Visual/Drawables/TestSceneLinearToBezier.cs rename to osu.Framework.Tests/Visual/Drawables/TestScenePiecewiseLinearToBezier.cs index 79f78db727..6239406b2c 100644 --- a/osu.Framework.Tests/Visual/Drawables/TestSceneLinearToBezier.cs +++ b/osu.Framework.Tests/Visual/Drawables/TestScenePiecewiseLinearToBezier.cs @@ -13,47 +13,47 @@ namespace osu.Framework.Tests.Visual.Drawables { - public partial class TestSceneLinearToBezier : GridTestScene + public partial class TestScenePiecewiseLinearToBezier : GridTestScene { private int numControlPoints; private int numTestPoints; private int maxIterations; - private readonly List doubleTests = new List(); + private readonly List doubleApproximatedPathTests = new List(); - public TestSceneLinearToBezier() + public TestScenePiecewiseLinearToBezier() : base(2, 2) { - doubleTests.Add(new DoubleApproximatedPathTest(PathApproximator.BezierToPiecewiseLinear, numControlPoints, numTestPoints, maxIterations)); + doubleApproximatedPathTests.Add(new DoubleApproximatedPathTest(PathApproximator.BezierToPiecewiseLinear, numControlPoints, numTestPoints, maxIterations)); Cell(0).AddRange(new[] { createLabel(nameof(PathApproximator.BezierToPiecewiseLinear)), new ApproximatedPathTest(PathApproximator.BezierToPiecewiseLinear), - doubleTests[^1], + doubleApproximatedPathTests[^1], }); - doubleTests.Add(new DoubleApproximatedPathTest(PathApproximator.CatmullToPiecewiseLinear, numControlPoints, numTestPoints, maxIterations)); + doubleApproximatedPathTests.Add(new DoubleApproximatedPathTest(PathApproximator.CatmullToPiecewiseLinear, numControlPoints, numTestPoints, maxIterations)); Cell(1).AddRange(new[] { createLabel(nameof(PathApproximator.CatmullToPiecewiseLinear)), new ApproximatedPathTest(PathApproximator.CatmullToPiecewiseLinear), - doubleTests[^1], + doubleApproximatedPathTests[^1], }); - doubleTests.Add(new DoubleApproximatedPathTest(PathApproximator.CircularArcToPiecewiseLinear, numControlPoints, numTestPoints, maxIterations)); + doubleApproximatedPathTests.Add(new DoubleApproximatedPathTest(PathApproximator.CircularArcToPiecewiseLinear, numControlPoints, numTestPoints, maxIterations)); Cell(2).AddRange(new[] { createLabel(nameof(PathApproximator.CircularArcToPiecewiseLinear)), new ApproximatedPathTest(PathApproximator.CircularArcToPiecewiseLinear), - doubleTests[^1], + doubleApproximatedPathTests[^1], }); - doubleTests.Add(new DoubleApproximatedPathTest(PathApproximator.LagrangePolynomialToPiecewiseLinear, numControlPoints, numTestPoints, maxIterations)); + doubleApproximatedPathTests.Add(new DoubleApproximatedPathTest(PathApproximator.LagrangePolynomialToPiecewiseLinear, numControlPoints, numTestPoints, maxIterations)); Cell(3).AddRange(new[] { createLabel(nameof(PathApproximator.LagrangePolynomialToPiecewiseLinear)), new ApproximatedPathTest(PathApproximator.LagrangePolynomialToPiecewiseLinear), - doubleTests[^1], + doubleApproximatedPathTests[^1], }); AddSliderStep($"{nameof(numControlPoints)}", 3, 25, 5, v => @@ -62,7 +62,7 @@ public TestSceneLinearToBezier() updateTests(); }); - AddSliderStep($"{nameof(numTestPoints)}", 10, 1000, 100, v => + AddSliderStep($"{nameof(numTestPoints)}", 10, 200, 100, v => { numTestPoints = v; updateTests(); @@ -76,7 +76,7 @@ public TestSceneLinearToBezier() AddStep("Enable optimization", () => { - foreach (var test in doubleTests) + foreach (var test in doubleApproximatedPathTests) test.OptimizePath = true; updateTests(); }); @@ -84,7 +84,7 @@ public TestSceneLinearToBezier() private void updateTests() { - foreach (var test in doubleTests) + foreach (var test in doubleApproximatedPathTests) { test.NumControlPoints = numControlPoints; test.NumTestPoints = numTestPoints; @@ -157,6 +157,7 @@ public DoubleApproximatedPathTest(ApproximatorFunc approximator, int numControlP public void UpdatePath() { if (!OptimizePath) return; + var controlPoints = PathApproximator.PiecewiseLinearToBezier(inputPath, NumControlPoints, NumTestPoints, MaxIterations); Vertices = PathApproximator.BezierToPiecewiseLinear(controlPoints.ToArray()); } diff --git a/osu.Framework/Utils/PathApproximator.cs b/osu.Framework/Utils/PathApproximator.cs index d30d61632e..f39ea798c4 100644 --- a/osu.Framework/Utils/PathApproximator.cs +++ b/osu.Framework/Utils/PathApproximator.cs @@ -337,7 +337,7 @@ public static List PiecewiseLinearToBezier(ReadOnlySpan inputP // Update labels to shift the distance distribution between points if (step % 11 == 0) - labels = interpolateWithDistanceDistribution(points, interpolator); + labels = interpolator.Interpolate(getDistanceDistribution(points)); // Calculate the gradient on the control points var diff = labels - points; @@ -372,11 +372,6 @@ private static NDArray getDistanceDistribution(NDArray points) return distCumulativeSum / distCumulativeSum[-1]; } - private static NDArray interpolateWithDistanceDistribution(NDArray points, Interpolator interpolator) - { - return interpolator.Interpolate(getDistanceDistribution(points)); - } - private class Interpolator { private readonly int ny; From f8f6a4c435517f64616efc7d81c07006e915f59c Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 18 Nov 2023 15:47:19 +0100 Subject: [PATCH 05/34] add benchmark --- .../BenchmarkPiecewiseLinearToBezier.cs | 43 +++++++++++++++++++ osu.Framework/Utils/PathApproximator.cs | 2 +- 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 osu.Framework.Benchmarks/BenchmarkPiecewiseLinearToBezier.cs diff --git a/osu.Framework.Benchmarks/BenchmarkPiecewiseLinearToBezier.cs b/osu.Framework.Benchmarks/BenchmarkPiecewiseLinearToBezier.cs new file mode 100644 index 0000000000..918f537d39 --- /dev/null +++ b/osu.Framework.Benchmarks/BenchmarkPiecewiseLinearToBezier.cs @@ -0,0 +1,43 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using BenchmarkDotNet.Attributes; +using osu.Framework.Utils; +using osuTK; + +namespace osu.Framework.Benchmarks +{ + public class BenchmarkPiecewiseLinearToBezier : BenchmarkTest + { + private Vector2[] inputPath = null!; + + [Params(5, 25)] + public int NumControlPoints; + + [Params(5, 200)] + public int NumTestPoints; + + [Params(0, 100, 200)] + public int MaxIterations; + + public override void SetUp() + { + base.SetUp(); + + Vector2[] points = new Vector2[5]; + points[0] = new Vector2(50, 250); + points[1] = new Vector2(150, 230); + points[2] = new Vector2(100, 150); + points[3] = new Vector2(200, 80); + points[4] = new Vector2(250, 50); + inputPath = PathApproximator.LagrangePolynomialToPiecewiseLinear(points).ToArray(); + } + + [Benchmark] + public List PiecewiseLinearToBezier() + { + return PathApproximator.PiecewiseLinearToBezier(inputPath, NumControlPoints, NumTestPoints, MaxIterations); + } + } +} diff --git a/osu.Framework/Utils/PathApproximator.cs b/osu.Framework/Utils/PathApproximator.cs index f39ea798c4..bbc444c846 100644 --- a/osu.Framework/Utils/PathApproximator.cs +++ b/osu.Framework/Utils/PathApproximator.cs @@ -306,7 +306,7 @@ public static List LagrangePolynomialToPiecewiseLinear(ReadOnlySpan PiecewiseLinearToBezier(ReadOnlySpan inputPath, int numControlPoints, int numTestPoints = 200, int maxIterations = 100) + public static List PiecewiseLinearToBezier(ReadOnlySpan inputPath, int numControlPoints, int numTestPoints = 100, int maxIterations = 100) { const float learning_rate = 8f; const float b1 = 0.9f; From b621984d78da9dfb30b9342a547bd0b5d8314c06 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 18 Nov 2023 16:45:17 +0100 Subject: [PATCH 06/34] numpy.net implementation --- osu.Framework/Utils/PathApproximator.cs | 73 ++++++++++--------------- osu.Framework/osu.Framework.csproj | 2 +- 2 files changed, 29 insertions(+), 46 deletions(-) diff --git a/osu.Framework/Utils/PathApproximator.cs b/osu.Framework/Utils/PathApproximator.cs index bbc444c846..6cabd24d60 100644 --- a/osu.Framework/Utils/PathApproximator.cs +++ b/osu.Framework/Utils/PathApproximator.cs @@ -6,7 +6,8 @@ using System.Linq; using osu.Framework.Graphics.Primitives; using osuTK; -using NumSharp; +using Numpy; +using Numpy.Models; namespace osu.Framework.Utils { @@ -323,17 +324,17 @@ public static List PiecewiseLinearToBezier(ReadOnlySpan inputP // Create initial control point positions equally spaced along the input path var controlPoints = interpolator.Interpolate(np.linspace(0, 1, numControlPoints)); - NDArray labels = null!; + NDarray labels = null!; // Initialize Adam optimizer variables var m = np.zeros_like(controlPoints); var v = np.zeros_like(controlPoints); var learnableMask = np.zeros_like(controlPoints); - learnableMask["1:-1"] = 1; + learnableMask["1:-1"] = np.ones_like(controlPoints["1:-1"]); for (int step = 0; step < maxIterations; step++) { - var points = np.matmul(weights, controlPoints); + var points = weights.matmul(controlPoints); // Update labels to shift the distance distribution between points if (step % 11 == 0) @@ -341,17 +342,17 @@ public static List PiecewiseLinearToBezier(ReadOnlySpan inputP // Calculate the gradient on the control points var diff = labels - points; - var grad = -1f / numControlPoints * np.matmul(weightsTranspose, diff); + var grad = -1f / numControlPoints * weightsTranspose.matmul(diff); // Apply learnable mask to prevent moving the endpoints grad *= learnableMask; // Update control points with Adam optimizer m = b1 * m + (1 - b1) * grad; - v = b2 * v + (1 - b2) * np.square(grad); + v = b2 * v + (1 - b2) * grad.square(); var mCorr = m / (1 - Math.Pow(b1, step + 1)); var vCorr = v / (1 - Math.Pow(b2, step + 1)); - controlPoints -= learning_rate * mCorr / (np.sqrt(vCorr) + epsilon); + controlPoints.isub(learning_rate * mCorr / (vCorr.sqrt() + epsilon)); } // Convert the resulting control points NDArray @@ -359,48 +360,42 @@ public static List PiecewiseLinearToBezier(ReadOnlySpan inputP for (int i = 0; i < numControlPoints; i++) { - result.Add(new Vector2(controlPoints[i, 0], controlPoints[i, 1])); + result.Add(new Vector2((float)controlPoints[i, 0], (float)controlPoints[i, 1])); } return result; } - private static NDArray getDistanceDistribution(NDArray points) + private static NDarray getDistanceDistribution(NDarray points) { - var distCumulativeSum = np.cumsum(np.sqrt(np.sum(np.square(points["1:"] - points[":-1"]), -1, NPTypeCode.Single)), typeCode: NPTypeCode.Single); - distCumulativeSum = np.concatenate(new[] { np.zeros(new Shape(1), NPTypeCode.Single), distCumulativeSum }); + var distCumulativeSum = (points["1:"] - points[":-1"]).square().sum(-1, np.float32).sqrt().cumsum(dtype: np.float32).pad(np.array(1, 0), "constant"); return distCumulativeSum / distCumulativeSum[-1]; } private class Interpolator { private readonly int ny; - private readonly NDArray ys; + private readonly NDarray ys; public Interpolator(ReadOnlySpan inputPath, int resolution = 1000) { - var arr = np.empty(new Shape(inputPath.Length, 2), NPTypeCode.Single); - - for (int i = 0; i < inputPath.Length; i++) - { - arr[i, 0] = inputPath[i].X; - arr[i, 1] = inputPath[i].Y; - } - + var arr = np.array(inputPath.ToArray(), dtype: np.float32); var dist = getDistanceDistribution(arr); ny = resolution; - ys = np.empty(new Shape(resolution, arr.shape[1]), NPTypeCode.Single); + ys = np.empty(new Shape(resolution, arr.shape[1]), np.float32); int current = 0; for (int i = 0; i < resolution; i++) { float target = (float)i / (resolution - 1); - while (dist[current] < target) + while ((float)dist[current] < target) current++; int prev = Math.Max(0, current - 1); - float t = (dist[current] - target) / (dist[current] - dist[prev]); + float currDist = (float)dist[current]; + float prevDist = (float)dist[prev]; + float t = (currDist - target) / (currDist - prevDist); if (float.IsNaN(t)) t = 0; @@ -409,46 +404,34 @@ public Interpolator(ReadOnlySpan inputPath, int resolution = 1000) } } - public NDArray Interpolate(NDArray x) + public NDarray Interpolate(NDarray x) { var xIdx = x * (ny - 1); - var idxBelow = np.floor(xIdx); - var idxAbove = np.minimum(idxBelow + 1, ny - 1); - idxBelow = np.maximum(idxAbove - 1, 0); + var idxBelow = xIdx.floor(); + var idxAbove = (idxBelow + 1).minimum(np.array(ny - 1)); + idxBelow = (idxAbove - 1).maximum(np.array(0)); var yRefBelow = ys[idxBelow]; var yRefAbove = ys[idxAbove]; var t = xIdx - idxBelow; - var y = t[Slice.All, np.newaxis] * yRefAbove + (1 - t)[Slice.All, np.newaxis] * yRefBelow; + var y = t[":, None"] * yRefAbove + (1 - t)[":, None"] * yRefBelow; return y; } } - private static NDArray generateBezierWeights(int numControlPoints, int numTestPoints) + private static NDarray generateBezierWeights(int numControlPoints, int numTestPoints) { var ts = np.linspace(0, 1, numTestPoints); - var coefficients = binomialCoefficients(numControlPoints).astype(NPTypeCode.Single); - var p = np.empty(new Shape(numTestPoints, numControlPoints), NPTypeCode.Single); - - for (int i = 0; i < numTestPoints; i++) - { - p[i, 0] = 1; - float t = ts[i]; - - for (int j = 1; j < numControlPoints; j++) - { - p[i, j] = p[i, j - 1] * t; - } - } - + var coefficients = np.array(binomialCoefficients(numControlPoints), dtype: np.float32); + var p = ts.power(np.arange(numControlPoints)); return coefficients * p["::-1, ::-1"] * p; } - private static NDArray binomialCoefficients(int n) + private static long[] binomialCoefficients(int n) { - var coefficients = np.empty(new Shape(n), NPTypeCode.Int64); + long[] coefficients = new long[n]; coefficients[0] = 1; for (int i = 1; i < (n + 1) / 2; i++) diff --git a/osu.Framework/osu.Framework.csproj b/osu.Framework/osu.Framework.csproj index 588e7ae589..d1a0e6b48c 100644 --- a/osu.Framework/osu.Framework.csproj +++ b/osu.Framework/osu.Framework.csproj @@ -24,7 +24,7 @@ - + From d9358d106f117f663a82ede324a0479165bec018 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 18 Nov 2023 19:56:27 +0100 Subject: [PATCH 07/34] Tensor.NET implementation --- osu.Framework/Utils/PathApproximator.cs | 123 ++++++++++++++++-------- osu.Framework/osu.Framework.csproj | 2 +- 2 files changed, 84 insertions(+), 41 deletions(-) diff --git a/osu.Framework/Utils/PathApproximator.cs b/osu.Framework/Utils/PathApproximator.cs index 6cabd24d60..2c89b4a02e 100644 --- a/osu.Framework/Utils/PathApproximator.cs +++ b/osu.Framework/Utils/PathApproximator.cs @@ -6,8 +6,8 @@ using System.Linq; using osu.Framework.Graphics.Primitives; using osuTK; -using Numpy; -using Numpy.Models; +using Tensornet; +using Tensornet.Math; namespace osu.Framework.Utils { @@ -317,24 +317,27 @@ public static List PiecewiseLinearToBezier(ReadOnlySpan inputP // Generate Bezier weight matrix var weights = generateBezierWeights(numControlPoints, numTestPoints); - var weightsTranspose = weights.T; + var weightsTranspose = weights.Transpose(0, 1); // Create efficient interpolation on the input path var interpolator = new Interpolator(inputPath, interpolator_resolution); // Create initial control point positions equally spaced along the input path - var controlPoints = interpolator.Interpolate(np.linspace(0, 1, numControlPoints)); - NDarray labels = null!; + var controlPoints = interpolator.Interpolate(Tensor.Linspace(0, 1, numControlPoints)); + Tensor labels = null!; // Initialize Adam optimizer variables - var m = np.zeros_like(controlPoints); - var v = np.zeros_like(controlPoints); - var learnableMask = np.zeros_like(controlPoints); - learnableMask["1:-1"] = np.ones_like(controlPoints["1:-1"]); + var m = Tensor.ZerosLike(controlPoints); + var v = Tensor.ZerosLike(controlPoints); + var learnableMask = Tensor.OnesLike(controlPoints); + learnableMask[0, 0] = 0; + learnableMask[0, 1] = 0; + learnableMask[numControlPoints - 1, 0] = 0; + learnableMask[numControlPoints - 1, 1] = 0; for (int step = 0; step < maxIterations; step++) { - var points = weights.matmul(controlPoints); + var points = weights.Matmul(controlPoints); // Update labels to shift the distance distribution between points if (step % 11 == 0) @@ -342,17 +345,17 @@ public static List PiecewiseLinearToBezier(ReadOnlySpan inputP // Calculate the gradient on the control points var diff = labels - points; - var grad = -1f / numControlPoints * weightsTranspose.matmul(diff); + var grad = -1f / numControlPoints * weightsTranspose.Matmul(diff); // Apply learnable mask to prevent moving the endpoints grad *= learnableMask; // Update control points with Adam optimizer m = b1 * m + (1 - b1) * grad; - v = b2 * v + (1 - b2) * grad.square(); - var mCorr = m / (1 - Math.Pow(b1, step + 1)); - var vCorr = v / (1 - Math.Pow(b2, step + 1)); - controlPoints.isub(learning_rate * mCorr / (vCorr.sqrt() + epsilon)); + v = b2 * v + (1 - b2) * MathT.Pow(grad, 2); + var mCorr = m / (float)(1 - Math.Pow(b1, step + 1)); + var vCorr = v / (float)(1 - Math.Pow(b2, step + 1)); + controlPoints -= learning_rate * mCorr / (MathT.Pow(vCorr, 0.5f) + epsilon); } // Convert the resulting control points NDArray @@ -360,78 +363,118 @@ public static List PiecewiseLinearToBezier(ReadOnlySpan inputP for (int i = 0; i < numControlPoints; i++) { - result.Add(new Vector2((float)controlPoints[i, 0], (float)controlPoints[i, 1])); + result.Add(new Vector2(controlPoints[i, 0], controlPoints[i, 1])); } return result; } - private static NDarray getDistanceDistribution(NDarray points) + private static Tensor getDistanceDistribution(Tensor points) { - var distCumulativeSum = (points["1:"] - points[":-1"]).square().sum(-1, np.float32).sqrt().cumsum(dtype: np.float32).pad(np.array(1, 0), "constant"); - return distCumulativeSum / distCumulativeSum[-1]; + var distCumulativeSum = MathT.Pow(MathT.Pow(points[1.., ..] - points[..^1, ..], 2).Sum(1), 0.5f); + float accumulator = 0; + distCumulativeSum.ForEachInplace(v => + { + accumulator += v; + return accumulator; + }); + distCumulativeSum = distCumulativeSum.Pad(new[] { (1, 0) }); + return distCumulativeSum / distCumulativeSum[distCumulativeSum.Shape[0] - 1]; } private class Interpolator { private readonly int ny; - private readonly NDarray ys; + private readonly Tensor ys; public Interpolator(ReadOnlySpan inputPath, int resolution = 1000) { - var arr = np.array(inputPath.ToArray(), dtype: np.float32); + var arr = Tensor.Zeros(new TensorShape(inputPath.Length, 2)); + + for (int i = 0; i < inputPath.Length; i++) + { + arr[i, 0] = inputPath[i].X; + arr[i, 1] = inputPath[i].Y; + } + var dist = getDistanceDistribution(arr); ny = resolution; - ys = np.empty(new Shape(resolution, arr.shape[1]), np.float32); + ys = Tensor.Zeros(new TensorShape(resolution, 2)); int current = 0; for (int i = 0; i < resolution; i++) { float target = (float)i / (resolution - 1); - while ((float)dist[current] < target) + while (dist[current] < target) current++; int prev = Math.Max(0, current - 1); - float currDist = (float)dist[current]; - float prevDist = (float)dist[prev]; + float currDist = dist[current]; + float prevDist = dist[prev]; float t = (currDist - target) / (currDist - prevDist); if (float.IsNaN(t)) t = 0; - ys[i] = t * arr[prev] + (1 - t) * arr[current]; + ys[i, 0] = t * arr[prev, 0] + (1 - t) * arr[current, 0]; + ys[i, 1] = t * arr[prev, 1] + (1 - t) * arr[current, 1]; } } - public NDarray Interpolate(NDarray x) + public Tensor Interpolate(Tensor x) { var xIdx = x * (ny - 1); - var idxBelow = xIdx.floor(); - var idxAbove = (idxBelow + 1).minimum(np.array(ny - 1)); - idxBelow = (idxAbove - 1).maximum(np.array(0)); + var idxBelow = xIdx.Floor(); + var idxAbove = MathT.Clamp(idxBelow + 1, 0, ny - 1); + idxBelow = MathT.Clamp(idxAbove - 1, 0, ny - 1); + + var yRefBelow = Tensor.Zeros(new TensorShape(x.Shape[0], 2)); + var yRefAbove = Tensor.Zeros(new TensorShape(x.Shape[0], 2)); - var yRefBelow = ys[idxBelow]; - var yRefAbove = ys[idxAbove]; + for (int i = 0; i < x.Shape[0]; i++) + { + int b = (int)idxBelow[i]; + int a = (int)idxAbove[i]; + yRefBelow[i, 0] = ys[b, 0]; + yRefBelow[i, 1] = ys[b, 1]; + yRefAbove[i, 0] = ys[a, 0]; + yRefAbove[i, 1] = ys[a, 1]; + } var t = xIdx - idxBelow; - var y = t[":, None"] * yRefAbove + (1 - t)[":, None"] * yRefBelow; + var y = t.Unsqueeze(1) * yRefAbove + (1 - t).Unsqueeze(1) * yRefBelow; return y; } } - private static NDarray generateBezierWeights(int numControlPoints, int numTestPoints) + private static Tensor generateBezierWeights(int numControlPoints, int numTestPoints) { - var ts = np.linspace(0, 1, numTestPoints); - var coefficients = np.array(binomialCoefficients(numControlPoints), dtype: np.float32); - var p = ts.power(np.arange(numControlPoints)); - return coefficients * p["::-1, ::-1"] * p; + var ts = Tensor.Linspace(0, 1, numTestPoints); + var coefficients = Tensor.FromArray(binomialCoefficients(numControlPoints), new[] { numControlPoints }); + float[] p = new float[numTestPoints * numControlPoints]; + + for (int i = 0; i < numTestPoints; i++) + { + p[i * numControlPoints] = 1; + float t = ts[i]; + + for (int j = 1; j < numControlPoints; j++) + { + p[i * numControlPoints + j] = p[i * numControlPoints + j - 1] * t; + } + } + + var pt = Tensor.FromArray(p, new TensorShape(numTestPoints, numControlPoints)); + var pti = pt.Rotate(2, 0, 1); + + return coefficients * pti * pt; } - private static long[] binomialCoefficients(int n) + private static float[] binomialCoefficients(int n) { - long[] coefficients = new long[n]; + float[] coefficients = new float[n]; coefficients[0] = 1; for (int i = 1; i < (n + 1) / 2; i++) diff --git a/osu.Framework/osu.Framework.csproj b/osu.Framework/osu.Framework.csproj index d1a0e6b48c..034762bc65 100644 --- a/osu.Framework/osu.Framework.csproj +++ b/osu.Framework/osu.Framework.csproj @@ -24,7 +24,6 @@ - @@ -53,5 +52,6 @@ + From adf7c83b7ce22ad2a6343ce8a3471553cb01bd90 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 20 Nov 2023 13:44:33 +0100 Subject: [PATCH 08/34] add optimized B-Spline basis function matrix --- osu.Framework/Utils/PathApproximator.cs | 31 +++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/osu.Framework/Utils/PathApproximator.cs b/osu.Framework/Utils/PathApproximator.cs index 2c89b4a02e..d518e4958a 100644 --- a/osu.Framework/Utils/PathApproximator.cs +++ b/osu.Framework/Utils/PathApproximator.cs @@ -449,6 +449,37 @@ public Tensor Interpolate(Tensor x) } } + /// + /// Calculate a matrix of B-spline basis function values. + /// + /// The number of control points. + /// The number of points to evaluate the spline at. + /// The order of the B-spline. + /// Matrix array of B-spline basis function values. + public static Tensor GenerateBSplineWeights(int numControlPoints, int numTestPoints, int degree) + { + // Calculate the basis function values using a modified vectorized De Boor's algorithm + var x = Tensor.Linspace(0, 1, numTestPoints).Unsqueeze(1); + var knots = Tensor.Linspace(0, 1, numControlPoints + 1 - degree).Pad(new[] { (degree, degree) }, constants: new[] { (0d, 1d) }); + (int, int)[] alphaPad = { (0, 0), (1, 0) }; + (int, int)[] betaPad = { (0, 0), (0, 1) }; + + // Calculate the first order basis + var prevOrder = Tensor.Zeros(new TensorShape(numTestPoints, numControlPoints - degree)); + for (int i = 0; i < numTestPoints; i++) + prevOrder[i, MathHelper.Clamp((int)(x[i, 0] * (numControlPoints - degree)), 0, numControlPoints - degree - 1)] = 1; + + // Calculate the higher order basis + for (int q = 1; q < degree + 1; q++) + { + var divisor = (knots[(degree + 1)..(numControlPoints + q)] - knots[(degree - q + 1)..numControlPoints]).Unsqueeze(0); + var alpha = (x - knots[(degree - q + 1)..numControlPoints].Unsqueeze(0)) / divisor; + prevOrder = (alpha * prevOrder).Pad(alphaPad) + ((1 - alpha) * prevOrder).Pad(betaPad); + } + + return prevOrder; + } + private static Tensor generateBezierWeights(int numControlPoints, int numTestPoints) { var ts = Tensor.Linspace(0, 1, numTestPoints); From 5d9b7891e52980e246f14c1f8ac027ba1cd6b881 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 20 Nov 2023 13:44:59 +0100 Subject: [PATCH 09/34] make method private --- osu.Framework/Utils/PathApproximator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Framework/Utils/PathApproximator.cs b/osu.Framework/Utils/PathApproximator.cs index d518e4958a..49ab34b033 100644 --- a/osu.Framework/Utils/PathApproximator.cs +++ b/osu.Framework/Utils/PathApproximator.cs @@ -456,7 +456,7 @@ public Tensor Interpolate(Tensor x) /// The number of points to evaluate the spline at. /// The order of the B-spline. /// Matrix array of B-spline basis function values. - public static Tensor GenerateBSplineWeights(int numControlPoints, int numTestPoints, int degree) + private static Tensor generateBSplineWeights(int numControlPoints, int numTestPoints, int degree) { // Calculate the basis function values using a modified vectorized De Boor's algorithm var x = Tensor.Linspace(0, 1, numTestPoints).Unsqueeze(1); From e4172c002dfaf6d862bf2f18c279ea614e15727a Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 20 Nov 2023 15:46:52 +0100 Subject: [PATCH 10/34] Added B-Spline optimization and test --- ...s => TestScenePiecewiseLinearToBSpline.cs} | 50 +++++++++++++++++-- osu.Framework/Utils/PathApproximator.cs | 23 ++++++--- 2 files changed, 60 insertions(+), 13 deletions(-) rename osu.Framework.Tests/Visual/Drawables/{TestScenePiecewiseLinearToBezier.cs => TestScenePiecewiseLinearToBSpline.cs} (80%) diff --git a/osu.Framework.Tests/Visual/Drawables/TestScenePiecewiseLinearToBezier.cs b/osu.Framework.Tests/Visual/Drawables/TestScenePiecewiseLinearToBSpline.cs similarity index 80% rename from osu.Framework.Tests/Visual/Drawables/TestScenePiecewiseLinearToBezier.cs rename to osu.Framework.Tests/Visual/Drawables/TestScenePiecewiseLinearToBSpline.cs index 6239406b2c..608f780bb7 100644 --- a/osu.Framework.Tests/Visual/Drawables/TestScenePiecewiseLinearToBezier.cs +++ b/osu.Framework.Tests/Visual/Drawables/TestScenePiecewiseLinearToBSpline.cs @@ -13,15 +13,19 @@ namespace osu.Framework.Tests.Visual.Drawables { - public partial class TestScenePiecewiseLinearToBezier : GridTestScene + public partial class TestScenePiecewiseLinearToBSpline : GridTestScene { private int numControlPoints; + private int degree; private int numTestPoints; private int maxIterations; + private float learningRate; + private float b1; + private float b2; private readonly List doubleApproximatedPathTests = new List(); - public TestScenePiecewiseLinearToBezier() + public TestScenePiecewiseLinearToBSpline() : base(2, 2) { doubleApproximatedPathTests.Add(new DoubleApproximatedPathTest(PathApproximator.BezierToPiecewiseLinear, numControlPoints, numTestPoints, maxIterations)); @@ -62,6 +66,12 @@ public TestScenePiecewiseLinearToBezier() updateTests(); }); + AddSliderStep($"{nameof(degree)}", 1, 5, 3, v => + { + degree = v; + updateTests(); + }); + AddSliderStep($"{nameof(numTestPoints)}", 10, 200, 100, v => { numTestPoints = v; @@ -74,6 +84,24 @@ public TestScenePiecewiseLinearToBezier() updateTests(); }); + AddSliderStep($"{nameof(learningRate)}", 0, 10, 8f, v => + { + learningRate = v; + updateTests(); + }); + + AddSliderStep($"{nameof(b1)}", 0, 0.999f, 0.8f, v => + { + b1 = v; + updateTests(); + }); + + AddSliderStep($"{nameof(b2)}", 0, 0.999f, 0.99f, v => + { + b2 = v; + updateTests(); + }); + AddStep("Enable optimization", () => { foreach (var test in doubleApproximatedPathTests) @@ -87,15 +115,19 @@ private void updateTests() foreach (var test in doubleApproximatedPathTests) { test.NumControlPoints = numControlPoints; + test.Degree = degree; test.NumTestPoints = numTestPoints; test.MaxIterations = maxIterations; + test.LearningRate = learningRate; + test.B1 = b1; + test.B2 = b2; test.UpdatePath(); } } private Drawable createLabel(string text) => new SpriteText { - Text = text, + Text = text + "ToBSpline", Font = new FontUsage(size: 20), Colour = Color4.White, }; @@ -127,10 +159,18 @@ private partial class DoubleApproximatedPathTest : SmoothPath public int NumControlPoints { get; set; } + public int Degree { get; set; } + public int NumTestPoints { get; set; } public int MaxIterations { get; set; } + public float LearningRate { get; set; } + + public float B1 { get; set; } + + public float B2 { get; set; } + public bool OptimizePath { get; set; } public DoubleApproximatedPathTest(ApproximatorFunc approximator, int numControlPoints, int numTestPoints, int maxIterations) @@ -158,8 +198,8 @@ public void UpdatePath() { if (!OptimizePath) return; - var controlPoints = PathApproximator.PiecewiseLinearToBezier(inputPath, NumControlPoints, NumTestPoints, MaxIterations); - Vertices = PathApproximator.BezierToPiecewiseLinear(controlPoints.ToArray()); + var controlPoints = PathApproximator.PiecewiseLinearToBSpline(inputPath, NumControlPoints, Degree, NumTestPoints, MaxIterations, LearningRate, B1, B2); + Vertices = PathApproximator.BSplineToPiecewiseLinear(controlPoints.ToArray(), Degree); } } } diff --git a/osu.Framework/Utils/PathApproximator.cs b/osu.Framework/Utils/PathApproximator.cs index 49ab34b033..0997cce2f7 100644 --- a/osu.Framework/Utils/PathApproximator.cs +++ b/osu.Framework/Utils/PathApproximator.cs @@ -307,20 +307,27 @@ public static List LagrangePolynomialToPiecewiseLinear(ReadOnlySpan PiecewiseLinearToBezier(ReadOnlySpan inputPath, int numControlPoints, int numTestPoints = 100, int maxIterations = 100) + public static List PiecewiseLinearToBezier(ReadOnlySpan inputPath, int numControlPoints, int numTestPoints = 100, int maxIterations = 100, float learningRate = 8f, float b1 = 0.9f, float b2 = 0.92f, int interpolatorResolution = 100) + { + return piecewiseLinearToSpline(inputPath, generateBezierWeights(numControlPoints, numTestPoints), maxIterations, learningRate, b1, b2, interpolatorResolution); + } + + public static List PiecewiseLinearToBSpline(ReadOnlySpan inputPath, int numControlPoints, int degree, int numTestPoints = 100, int maxIterations = 100, float learningRate = 8f, float b1 = 0.8f, float b2 = 0.99f, int interpolatorResolution = 100) + { + degree = Math.Min(degree, numControlPoints - 1); + return piecewiseLinearToSpline(inputPath, generateBSplineWeights(numControlPoints, numTestPoints, degree), maxIterations, learningRate, b1, b2, interpolatorResolution); + } + + private static List piecewiseLinearToSpline(ReadOnlySpan inputPath, Tensor weights, int maxIterations = 100, float learningRate = 8f, float b1 = 0.9f, float b2 = 0.92f, int interpolatorResolution = 100) { - const float learning_rate = 8f; - const float b1 = 0.9f; - const float b2 = 0.92f; const float epsilon = 1E-8f; - const int interpolator_resolution = 100; // Generate Bezier weight matrix - var weights = generateBezierWeights(numControlPoints, numTestPoints); + int numControlPoints = weights.Shape[1]; var weightsTranspose = weights.Transpose(0, 1); // Create efficient interpolation on the input path - var interpolator = new Interpolator(inputPath, interpolator_resolution); + var interpolator = new Interpolator(inputPath, interpolatorResolution); // Create initial control point positions equally spaced along the input path var controlPoints = interpolator.Interpolate(Tensor.Linspace(0, 1, numControlPoints)); @@ -355,7 +362,7 @@ public static List PiecewiseLinearToBezier(ReadOnlySpan inputP v = b2 * v + (1 - b2) * MathT.Pow(grad, 2); var mCorr = m / (float)(1 - Math.Pow(b1, step + 1)); var vCorr = v / (float)(1 - Math.Pow(b2, step + 1)); - controlPoints -= learning_rate * mCorr / (MathT.Pow(vCorr, 0.5f) + epsilon); + controlPoints -= learningRate * mCorr / (MathT.Pow(vCorr, 0.5f) + epsilon); } // Convert the resulting control points NDArray From 345b81a011d0bd6de4982a03a3d118617a096a72 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 20 Nov 2023 18:37:14 +0100 Subject: [PATCH 11/34] add initial control points arg --- osu.Framework/Utils/PathApproximator.cs | 52 +++++++++++++++++++++---- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/osu.Framework/Utils/PathApproximator.cs b/osu.Framework/Utils/PathApproximator.cs index 0997cce2f7..16b9b5ee46 100644 --- a/osu.Framework/Utils/PathApproximator.cs +++ b/osu.Framework/Utils/PathApproximator.cs @@ -307,18 +307,42 @@ public static List LagrangePolynomialToPiecewiseLinear(ReadOnlySpan PiecewiseLinearToBezier(ReadOnlySpan inputPath, int numControlPoints, int numTestPoints = 100, int maxIterations = 100, float learningRate = 8f, float b1 = 0.9f, float b2 = 0.92f, int interpolatorResolution = 100) + public static List PiecewiseLinearToBezier(ReadOnlySpan inputPath, + int numControlPoints, + int numTestPoints = 100, + int maxIterations = 100, + float learningRate = 8f, + float b1 = 0.9f, + float b2 = 0.92f, + int interpolatorResolution = 100, + List? initialControlPoints = null) { - return piecewiseLinearToSpline(inputPath, generateBezierWeights(numControlPoints, numTestPoints), maxIterations, learningRate, b1, b2, interpolatorResolution); + return piecewiseLinearToSpline(inputPath, generateBezierWeights(numControlPoints, numTestPoints), maxIterations, learningRate, b1, b2, interpolatorResolution, initialControlPoints); } - public static List PiecewiseLinearToBSpline(ReadOnlySpan inputPath, int numControlPoints, int degree, int numTestPoints = 100, int maxIterations = 100, float learningRate = 8f, float b1 = 0.8f, float b2 = 0.99f, int interpolatorResolution = 100) + public static List PiecewiseLinearToBSpline(ReadOnlySpan inputPath, + int numControlPoints, + int degree, + int numTestPoints = 100, + int maxIterations = 100, + float learningRate = 8f, + float b1 = 0.8f, + float b2 = 0.99f, + int interpolatorResolution = 100, + List? initialControlPoints = null) { degree = Math.Min(degree, numControlPoints - 1); - return piecewiseLinearToSpline(inputPath, generateBSplineWeights(numControlPoints, numTestPoints, degree), maxIterations, learningRate, b1, b2, interpolatorResolution); + return piecewiseLinearToSpline(inputPath, generateBSplineWeights(numControlPoints, numTestPoints, degree), maxIterations, learningRate, b1, b2, interpolatorResolution, initialControlPoints); } - private static List piecewiseLinearToSpline(ReadOnlySpan inputPath, Tensor weights, int maxIterations = 100, float learningRate = 8f, float b1 = 0.9f, float b2 = 0.92f, int interpolatorResolution = 100) + private static List piecewiseLinearToSpline(ReadOnlySpan inputPath, + Tensor weights, + int maxIterations = 100, + float learningRate = 8f, + float b1 = 0.9f, + float b2 = 0.92f, + int interpolatorResolution = 100, + List? initialControlPoints = null) { const float epsilon = 1E-8f; @@ -329,9 +353,23 @@ private static List piecewiseLinearToSpline(ReadOnlySpan input // Create efficient interpolation on the input path var interpolator = new Interpolator(inputPath, interpolatorResolution); - // Create initial control point positions equally spaced along the input path - var controlPoints = interpolator.Interpolate(Tensor.Linspace(0, 1, numControlPoints)); + // Initialize control points Tensor labels = null!; + Tensor controlPoints; + + if (initialControlPoints is not null) + { + controlPoints = Tensor.Zeros(new TensorShape(numControlPoints, 2)); + + for (int i = 0; i < numControlPoints; i++) + { + controlPoints[i, 0] = initialControlPoints[i].X; + controlPoints[i, 1] = initialControlPoints[i].Y; + } + } + else + // Create initial control point positions equally spaced along the input path + controlPoints = interpolator.Interpolate(Tensor.Linspace(0, 1, numControlPoints)); // Initialize Adam optimizer variables var m = Tensor.ZerosLike(controlPoints); From f847b3f67634e74038cde3a09dc4b504be0d61c9 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 20 Nov 2023 18:37:43 +0100 Subject: [PATCH 12/34] Add naive use of optimizer to IncrementalBSplineBuilder --- osu.Framework/Utils/IncrementalBSplineBuilder.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Framework/Utils/IncrementalBSplineBuilder.cs b/osu.Framework/Utils/IncrementalBSplineBuilder.cs index fbab87371b..6095805d63 100644 --- a/osu.Framework/Utils/IncrementalBSplineBuilder.cs +++ b/osu.Framework/Utils/IncrementalBSplineBuilder.cs @@ -283,12 +283,10 @@ private void regenerateApproximatedPathControlPoints() return; } - controlPoints.Value = new List(); - Debug.Assert(vertices.Count == distances.Count + 1); var cornerTs = detectCorners(vertices, distances); - var cps = controlPoints.Value; + var cps = new List(); cps.Add(vertices[0]); // Populate each segment between corners with control points that have density proportional to the @@ -354,6 +352,8 @@ private void regenerateApproximatedPathControlPoints() else cps.AddRange(Enumerable.Repeat(c1, degree)); } + + controlPoints.Value = PathApproximator.PiecewiseLinearToBSpline(inputPath.ToArray(), cps.Count, degree, initialControlPoints: cps); } private void redrawApproximatedPath() From c112ed08afd77227705291397cf5f315b3762675 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 20 Nov 2023 18:52:03 +0100 Subject: [PATCH 13/34] use smooth path for shape --- osu.Framework/Utils/IncrementalBSplineBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Framework/Utils/IncrementalBSplineBuilder.cs b/osu.Framework/Utils/IncrementalBSplineBuilder.cs index 6095805d63..f4b0fbea75 100644 --- a/osu.Framework/Utils/IncrementalBSplineBuilder.cs +++ b/osu.Framework/Utils/IncrementalBSplineBuilder.cs @@ -353,7 +353,7 @@ private void regenerateApproximatedPathControlPoints() cps.AddRange(Enumerable.Repeat(c1, degree)); } - controlPoints.Value = PathApproximator.PiecewiseLinearToBSpline(inputPath.ToArray(), cps.Count, degree, initialControlPoints: cps); + controlPoints.Value = PathApproximator.PiecewiseLinearToBSpline(vertices.ToArray(), cps.Count, degree, initialControlPoints: cps); } private void redrawApproximatedPath() From 67243055867334c07c5fae88d48641e60ac95408 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 20 Nov 2023 23:46:26 +0100 Subject: [PATCH 14/34] increase max iterations --- osu.Framework/Utils/IncrementalBSplineBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Framework/Utils/IncrementalBSplineBuilder.cs b/osu.Framework/Utils/IncrementalBSplineBuilder.cs index f4b0fbea75..119a055f8e 100644 --- a/osu.Framework/Utils/IncrementalBSplineBuilder.cs +++ b/osu.Framework/Utils/IncrementalBSplineBuilder.cs @@ -353,7 +353,7 @@ private void regenerateApproximatedPathControlPoints() cps.AddRange(Enumerable.Repeat(c1, degree)); } - controlPoints.Value = PathApproximator.PiecewiseLinearToBSpline(vertices.ToArray(), cps.Count, degree, initialControlPoints: cps); + controlPoints.Value = PathApproximator.PiecewiseLinearToBSpline(vertices.ToArray(), cps.Count, degree, maxIterations: 200, initialControlPoints: cps); } private void redrawApproximatedPath() From 27e5a9de82f47a02f8a774a1ef85078fa21fcf86 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 21 Nov 2023 20:53:30 +0100 Subject: [PATCH 15/34] remove tensor.net and implement with pure c# --- .../TestScenePiecewiseLinearToBSpline.cs | 31 +- osu.Framework/Utils/PathApproximator.cs | 328 +++++++++++++----- osu.Framework/osu.Framework.csproj | 1 - 3 files changed, 259 insertions(+), 101 deletions(-) diff --git a/osu.Framework.Tests/Visual/Drawables/TestScenePiecewiseLinearToBSpline.cs b/osu.Framework.Tests/Visual/Drawables/TestScenePiecewiseLinearToBSpline.cs index 608f780bb7..f2b001e1e3 100644 --- a/osu.Framework.Tests/Visual/Drawables/TestScenePiecewiseLinearToBSpline.cs +++ b/osu.Framework.Tests/Visual/Drawables/TestScenePiecewiseLinearToBSpline.cs @@ -15,20 +15,20 @@ namespace osu.Framework.Tests.Visual.Drawables { public partial class TestScenePiecewiseLinearToBSpline : GridTestScene { - private int numControlPoints; - private int degree; - private int numTestPoints; - private int maxIterations; - private float learningRate; - private float b1; - private float b2; + private int numControlPoints = 5; + private int degree = 2; + private int numTestPoints = 100; + private int maxIterations = 100; + private float learningRate = 8; + private float b1 = 0.8f; + private float b2 = 0.99f; private readonly List doubleApproximatedPathTests = new List(); public TestScenePiecewiseLinearToBSpline() : base(2, 2) { - doubleApproximatedPathTests.Add(new DoubleApproximatedPathTest(PathApproximator.BezierToPiecewiseLinear, numControlPoints, numTestPoints, maxIterations)); + doubleApproximatedPathTests.Add(new DoubleApproximatedPathTest(PathApproximator.BezierToPiecewiseLinear)); Cell(0).AddRange(new[] { createLabel(nameof(PathApproximator.BezierToPiecewiseLinear)), @@ -36,7 +36,7 @@ public TestScenePiecewiseLinearToBSpline() doubleApproximatedPathTests[^1], }); - doubleApproximatedPathTests.Add(new DoubleApproximatedPathTest(PathApproximator.CatmullToPiecewiseLinear, numControlPoints, numTestPoints, maxIterations)); + doubleApproximatedPathTests.Add(new DoubleApproximatedPathTest(PathApproximator.CatmullToPiecewiseLinear)); Cell(1).AddRange(new[] { createLabel(nameof(PathApproximator.CatmullToPiecewiseLinear)), @@ -44,7 +44,7 @@ public TestScenePiecewiseLinearToBSpline() doubleApproximatedPathTests[^1], }); - doubleApproximatedPathTests.Add(new DoubleApproximatedPathTest(PathApproximator.CircularArcToPiecewiseLinear, numControlPoints, numTestPoints, maxIterations)); + doubleApproximatedPathTests.Add(new DoubleApproximatedPathTest(PathApproximator.CircularArcToPiecewiseLinear)); Cell(2).AddRange(new[] { createLabel(nameof(PathApproximator.CircularArcToPiecewiseLinear)), @@ -52,7 +52,7 @@ public TestScenePiecewiseLinearToBSpline() doubleApproximatedPathTests[^1], }); - doubleApproximatedPathTests.Add(new DoubleApproximatedPathTest(PathApproximator.LagrangePolynomialToPiecewiseLinear, numControlPoints, numTestPoints, maxIterations)); + doubleApproximatedPathTests.Add(new DoubleApproximatedPathTest(PathApproximator.LagrangePolynomialToPiecewiseLinear)); Cell(3).AddRange(new[] { createLabel(nameof(PathApproximator.LagrangePolynomialToPiecewiseLinear)), @@ -173,7 +173,7 @@ private partial class DoubleApproximatedPathTest : SmoothPath public bool OptimizePath { get; set; } - public DoubleApproximatedPathTest(ApproximatorFunc approximator, int numControlPoints, int numTestPoints, int maxIterations) + public DoubleApproximatedPathTest(ApproximatorFunc approximator) { Vector2[] points = new Vector2[5]; points[0] = new Vector2(50, 250); @@ -186,19 +186,14 @@ public DoubleApproximatedPathTest(ApproximatorFunc approximator, int numControlP RelativeSizeAxes = Axes.Both; PathRadius = 2; Colour = Color4.Magenta; - - NumControlPoints = numControlPoints; - NumTestPoints = numTestPoints; - MaxIterations = maxIterations; inputPath = approximator(points).ToArray(); - UpdatePath(); } public void UpdatePath() { if (!OptimizePath) return; - var controlPoints = PathApproximator.PiecewiseLinearToBSpline(inputPath, NumControlPoints, Degree, NumTestPoints, MaxIterations, LearningRate, B1, B2); + var controlPoints = PathApproximator.PiecewiseLinearToBSpline(inputPath, NumControlPoints, Degree, NumTestPoints, MaxIterations); Vertices = PathApproximator.BSplineToPiecewiseLinear(controlPoints.ToArray(), Degree); } } diff --git a/osu.Framework/Utils/PathApproximator.cs b/osu.Framework/Utils/PathApproximator.cs index 8a194e3a4e..740827394c 100644 --- a/osu.Framework/Utils/PathApproximator.cs +++ b/osu.Framework/Utils/PathApproximator.cs @@ -6,8 +6,6 @@ using System.Linq; using osu.Framework.Graphics.Primitives; using osuTK; -using Tensornet; -using Tensornet.Math; namespace osu.Framework.Utils { @@ -328,7 +326,7 @@ public static List PiecewiseLinearToBSpline(ReadOnlySpan input } private static List piecewiseLinearToSpline(ReadOnlySpan inputPath, - Tensor weights, + float[,] weights, int maxIterations = 100, float learningRate = 8f, float b1 = 0.9f, @@ -336,23 +334,19 @@ private static List piecewiseLinearToSpline(ReadOnlySpan input int interpolatorResolution = 100, List? initialControlPoints = null) { - const float epsilon = 1E-8f; - // Generate Bezier weight matrix - int numControlPoints = weights.Shape[1]; - var weightsTranspose = weights.Transpose(0, 1); + int numControlPoints = weights.GetLength(1); + int numTestPoints = weights.GetLength(0); // Create efficient interpolation on the input path var interpolator = new Interpolator(inputPath, interpolatorResolution); // Initialize control points - Tensor labels = null!; - Tensor controlPoints; + float[,] labels = new float[numTestPoints, 2]; + float[,] controlPoints = new float[numControlPoints, 2]; if (initialControlPoints is not null) { - controlPoints = Tensor.Zeros(new TensorShape(numControlPoints, 2)); - for (int i = 0; i < numControlPoints; i++) { controlPoints[i, 0] = initialControlPoints[i].X; @@ -361,38 +355,48 @@ private static List piecewiseLinearToSpline(ReadOnlySpan input } else // Create initial control point positions equally spaced along the input path - controlPoints = interpolator.Interpolate(Tensor.Linspace(0, 1, numControlPoints)); + interpolator.Interpolate(linspace(0, 1, numControlPoints), controlPoints); // Initialize Adam optimizer variables - var m = Tensor.ZerosLike(controlPoints); - var v = Tensor.ZerosLike(controlPoints); - var learnableMask = Tensor.OnesLike(controlPoints); - learnableMask[0, 0] = 0; - learnableMask[0, 1] = 0; - learnableMask[numControlPoints - 1, 0] = 0; - learnableMask[numControlPoints - 1, 1] = 0; + float[,] m = new float[numControlPoints, 2]; + float[,] v = new float[numControlPoints, 2]; + float[,] learnableMask = new float[numControlPoints, 2]; + + for (int i = 1; i < numControlPoints - 1; i++) + { + learnableMask[i, 0] = 1; + learnableMask[i, 1] = 1; + } + + // Initialize intermediate variables + float[,] points = new float[numTestPoints, 2]; + float[,] grad = new float[numControlPoints, 2]; + float[] distanceDistribution = new float[numTestPoints]; for (int step = 0; step < maxIterations; step++) { - var points = weights.Matmul(controlPoints); + matmul(weights, controlPoints, points); // Update labels to shift the distance distribution between points if (step % 11 == 0) - labels = interpolator.Interpolate(getDistanceDistribution(points)); + { + getDistanceDistribution(points, distanceDistribution); + interpolator.Interpolate(distanceDistribution, labels); + } // Calculate the gradient on the control points - var diff = labels - points; - var grad = -1f / numControlPoints * weightsTranspose.Matmul(diff); + matDiff(labels, points, points); + matmulTranspose(weights, points, grad); + matScale(grad, -1f / numControlPoints, grad); // Apply learnable mask to prevent moving the endpoints - grad *= learnableMask; + matProduct(grad, learnableMask, grad); // Update control points with Adam optimizer - m = b1 * m + (1 - b1) * grad; - v = b2 * v + (1 - b2) * MathT.Pow(grad, 2); - var mCorr = m / (float)(1 - Math.Pow(b1, step + 1)); - var vCorr = v / (float)(1 - Math.Pow(b2, step + 1)); - controlPoints -= learningRate * mCorr / (MathT.Pow(vCorr, 0.5f) + epsilon); + matLerp(grad, m, b1, m); + matProduct(grad, grad, grad); + matLerp(grad, v, b2, v); + adamUpdate(controlPoints, m, v, step, learningRate, b1, b2); } // Convert the resulting control points NDArray @@ -406,27 +410,164 @@ private static List piecewiseLinearToSpline(ReadOnlySpan input return result; } - private static Tensor getDistanceDistribution(Tensor points) + private static void adamUpdate(float[,] parameters, float[,] m, float[,] v, int step, float learningRate, float b1, float b2) { - var distCumulativeSum = MathT.Pow(MathT.Pow(points[1.., ..] - points[..^1, ..], 2).Sum(1), 0.5f); + const float epsilon = 1E-8f; + float mMult = 1 / (1 - MathF.Pow(b1, step + 1)); + float vMult = 1 / (1 - MathF.Pow(b2, step + 1)); + int m0 = m.GetLength(0); + int m1 = m.GetLength(1); + + for (int i = 0; i < m0; i++) + { + for (int j = 0; j < m1; j++) + { + float mCorr = m[i, j] * mMult; + float vCorr = v[i, j] * vMult; + parameters[i, j] -= learningRate * mCorr / (MathF.Sqrt(vCorr) + epsilon); + } + } + } + + private static void matLerp(float[,] mat1, float[,] mat2, float t, float[,] result) + { + int m = mat1.GetLength(0); + int n = mat1.GetLength(1); + + for (int i = 0; i < m; i++) + { + for (int j = 0; j < n; j++) + { + result[i, j] = mat1[i, j] * (1 - t) + mat2[i, j] * t; + } + } + } + + private static void matProduct(float[,] mat1, float[,] mat2, float[,] result) + { + int m = mat1.GetLength(0); + int n = mat1.GetLength(1); + + for (int i = 0; i < m; i++) + { + for (int j = 0; j < n; j++) + { + result[i, j] = mat1[i, j] * mat2[i, j]; + } + } + } + + private static void matScale(float[,] mat, float scalar, float[,] result) + { + int m = mat.GetLength(0); + int n = mat.GetLength(1); + + for (int i = 0; i < m; i++) + { + for (int j = 0; j < n; j++) + { + result[i, j] = mat[i, j] * scalar; + } + } + } + + private static void matmulTranspose(float[,] mat1, float[,] mat2, float[,] result) + { + int m = mat1.GetLength(1); + int n = mat2.GetLength(1); + int p = mat1.GetLength(0); + + for (int i = 0; i < m; i++) + { + for (int j = 0; j < n; j++) + { + float sum = 0; + + for (int k = 0; k < p; k++) + { + sum += mat1[k, i] * mat2[k, j]; + } + + result[i, j] = sum; + } + } + } + + private static void matDiff(float[,] mat1, float[,] mat2, float[,] result) + { + int m = mat1.GetLength(0); + int n = mat1.GetLength(1); + + for (int i = 0; i < m; i++) + { + for (int j = 0; j < n; j++) + { + result[i, j] = mat1[i, j] - mat2[i, j]; + } + } + } + + private static void matmul(float[,] mat1, float[,] mat2, float[,] result) + { + int m = mat1.GetLength(0); + int n = mat2.GetLength(1); + int p = mat1.GetLength(1); + + for (int i = 0; i < m; i++) + { + for (int j = 0; j < n; j++) + { + float sum = 0; + + for (int k = 0; k < p; k++) + { + sum += mat1[i, k] * mat2[k, j]; + } + + result[i, j] = sum; + } + } + } + + private static float[] linspace(float start, float end, int count) + { + float[] result = new float[count]; + + for (int i = 0; i < count; i++) + { + result[i] = start + (end - start) * i / (count - 1); + } + + return result; + } + + private static void getDistanceDistribution(float[,] points, float[] result) + { + int m = points.GetLength(0); float accumulator = 0; - distCumulativeSum.ForEachInplace(v => + result[0] = 0; + + for (int i = 1; i < m; i++) + { + float dist = MathF.Sqrt(MathF.Pow(points[i, 0] - points[i - 1, 0], 2) + MathF.Pow(points[i, 1] - points[i - 1, 1], 2)); + accumulator += dist; + result[i] = accumulator; + } + + for (int i = 0; i < m; i++) { - accumulator += v; - return accumulator; - }); - distCumulativeSum = distCumulativeSum.Pad(new[] { (1, 0) }); - return distCumulativeSum / distCumulativeSum[distCumulativeSum.Shape[0] - 1]; + result[i] /= accumulator; + } } private class Interpolator { private readonly int ny; - private readonly Tensor ys; + private readonly float[,] ys; public Interpolator(ReadOnlySpan inputPath, int resolution = 1000) { - var arr = Tensor.Zeros(new TensorShape(inputPath.Length, 2)); + float[,] arr = new float[inputPath.Length, 2]; for (int i = 0; i < inputPath.Length; i++) { @@ -434,9 +575,10 @@ public Interpolator(ReadOnlySpan inputPath, int resolution = 1000) arr[i, 1] = inputPath[i].Y; } - var dist = getDistanceDistribution(arr); + float[] dist = new float[inputPath.Length]; + getDistanceDistribution(arr, dist); ny = resolution; - ys = Tensor.Zeros(new TensorShape(resolution, 2)); + ys = new float[resolution, 2]; int current = 0; for (int i = 0; i < resolution; i++) @@ -459,30 +601,22 @@ public Interpolator(ReadOnlySpan inputPath, int resolution = 1000) } } - public Tensor Interpolate(Tensor x) + public void Interpolate(float[] x, float[,] result) { - var xIdx = x * (ny - 1); - var idxBelow = xIdx.Floor(); - var idxAbove = MathT.Clamp(idxBelow + 1, 0, ny - 1); - idxBelow = MathT.Clamp(idxAbove - 1, 0, ny - 1); - - var yRefBelow = Tensor.Zeros(new TensorShape(x.Shape[0], 2)); - var yRefAbove = Tensor.Zeros(new TensorShape(x.Shape[0], 2)); + int nx = x.Length; - for (int i = 0; i < x.Shape[0]; i++) + for (int i = 0; i < nx; i++) { - int b = (int)idxBelow[i]; - int a = (int)idxAbove[i]; - yRefBelow[i, 0] = ys[b, 0]; - yRefBelow[i, 1] = ys[b, 1]; - yRefAbove[i, 0] = ys[a, 0]; - yRefAbove[i, 1] = ys[a, 1]; - } + float idx = x[i] * (ny - 1); + int idxBelow = (int)idx; + int idxAbove = Math.Min(idxBelow + 1, ny - 1); + idxBelow = Math.Max(idxAbove - 1, 0); - var t = xIdx - idxBelow; + float t = idx - idxBelow; - var y = t.Unsqueeze(1) * yRefAbove + (1 - t).Unsqueeze(1) * yRefBelow; - return y; + result[i, 0] = t * ys[idxAbove, 0] + (1 - t) * ys[idxBelow, 0]; + result[i, 1] = t * ys[idxAbove, 1] + (1 - t) * ys[idxBelow, 1]; + } } } @@ -493,56 +627,86 @@ public Tensor Interpolate(Tensor x) /// The number of points to evaluate the spline at. /// The order of the B-spline. /// Matrix array of B-spline basis function values. - private static Tensor generateBSplineWeights(int numControlPoints, int numTestPoints, int degree) + private static float[,] generateBSplineWeights(int numControlPoints, int numTestPoints, int degree) { // Calculate the basis function values using a modified vectorized De Boor's algorithm - var x = Tensor.Linspace(0, 1, numTestPoints).Unsqueeze(1); - var knots = Tensor.Linspace(0, 1, numControlPoints + 1 - degree).Pad(new[] { (degree, degree) }, constants: new[] { (0d, 1d) }); - (int, int)[] alphaPad = { (0, 0), (1, 0) }; - (int, int)[] betaPad = { (0, 0), (0, 1) }; + float[] x = linspace(0, 1, numTestPoints); + float[] knots = new float[numControlPoints + degree + 1]; + + for (int i = 0; i < degree; i++) + { + knots[i] = 0; + knots[numControlPoints + degree - i] = 1; + } + + for (int i = degree; i < numControlPoints + 1; i++) + { + knots[i] = (float)(i - degree) / (numControlPoints - degree); + } // Calculate the first order basis - var prevOrder = Tensor.Zeros(new TensorShape(numTestPoints, numControlPoints - degree)); + float[,] prevOrder = new float[numTestPoints, numControlPoints]; + for (int i = 0; i < numTestPoints; i++) - prevOrder[i, MathHelper.Clamp((int)(x[i, 0] * (numControlPoints - degree)), 0, numControlPoints - degree - 1)] = 1; + { + prevOrder[i, (int)MathHelper.Clamp(x[i] * (numControlPoints - degree), 0, numControlPoints - degree - 1)] = 1; + } // Calculate the higher order basis for (int q = 1; q < degree + 1; q++) { - var divisor = (knots[(degree + 1)..(numControlPoints + q)] - knots[(degree - q + 1)..numControlPoints]).Unsqueeze(0); - var alpha = (x - knots[(degree - q + 1)..numControlPoints].Unsqueeze(0)) / divisor; - prevOrder = (alpha * prevOrder).Pad(alphaPad) + ((1 - alpha) * prevOrder).Pad(betaPad); + for (int i = 0; i < numTestPoints; i++) + { + float prevAlpha = 0; + + for (int j = 0; j < numControlPoints - degree + q - 1; j++) + { + float alpha = (x[i] - knots[degree - q + 1 + j]) / (knots[degree + 1 + j] - knots[degree - q + 1 + j]); + float alphaVal = alpha * prevOrder[i, j]; + float betaVal = (1 - alpha) * prevOrder[i, j]; + prevOrder[i, j] = prevAlpha + betaVal; + prevAlpha = alphaVal; + } + + prevOrder[i, numControlPoints - degree + q - 1] = prevAlpha; + } } return prevOrder; } - private static Tensor generateBezierWeights(int numControlPoints, int numTestPoints) + private static float[,] generateBezierWeights(int numControlPoints, int numTestPoints) { - var ts = Tensor.Linspace(0, 1, numTestPoints); - var coefficients = Tensor.FromArray(binomialCoefficients(numControlPoints), new[] { numControlPoints }); - float[] p = new float[numTestPoints * numControlPoints]; + long[] coefficients = binomialCoefficients(numControlPoints); + float[,] p = new float[numTestPoints, numControlPoints]; for (int i = 0; i < numTestPoints; i++) { - p[i * numControlPoints] = 1; - float t = ts[i]; + p[i, 0] = 1; + float t = (float)i / (numTestPoints - 1); for (int j = 1; j < numControlPoints; j++) { - p[i * numControlPoints + j] = p[i * numControlPoints + j - 1] * t; + p[i, j] = p[i, j - 1] * t; } } - var pt = Tensor.FromArray(p, new TensorShape(numTestPoints, numControlPoints)); - var pti = pt.Rotate(2, 0, 1); + float[,] result = new float[numTestPoints, numControlPoints]; - return coefficients * pti * pt; + for (int i = 0; i < numTestPoints; i++) + { + for (int j = 0; j < numControlPoints; j++) + { + result[i, j] = coefficients[j] * p[i, j] * p[numTestPoints - i - 1, numControlPoints - j - 1]; + } + } + + return result; } - private static float[] binomialCoefficients(int n) + private static long[] binomialCoefficients(int n) { - float[] coefficients = new float[n]; + long[] coefficients = new long[n]; coefficients[0] = 1; for (int i = 1; i < (n + 1) / 2; i++) diff --git a/osu.Framework/osu.Framework.csproj b/osu.Framework/osu.Framework.csproj index 034762bc65..3ebc16d4f8 100644 --- a/osu.Framework/osu.Framework.csproj +++ b/osu.Framework/osu.Framework.csproj @@ -52,6 +52,5 @@ - From c7745ae0ffabe617ef377f1a27c0d16f414d1b2c Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 22 Nov 2023 00:17:50 +0100 Subject: [PATCH 16/34] code quality fixes --- osu.Framework/Utils/IncrementalBSplineBuilder.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Framework/Utils/IncrementalBSplineBuilder.cs b/osu.Framework/Utils/IncrementalBSplineBuilder.cs index 119a055f8e..2718106c44 100644 --- a/osu.Framework/Utils/IncrementalBSplineBuilder.cs +++ b/osu.Framework/Utils/IncrementalBSplineBuilder.cs @@ -286,8 +286,7 @@ private void regenerateApproximatedPathControlPoints() Debug.Assert(vertices.Count == distances.Count + 1); var cornerTs = detectCorners(vertices, distances); - var cps = new List(); - cps.Add(vertices[0]); + var cps = new List { vertices[0] }; // Populate each segment between corners with control points that have density proportional to the // product of Tolerance and curvature. From 0ef83097e6710e55bb19678e9d3d3a0065047c2f Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 22 Nov 2023 00:18:11 +0100 Subject: [PATCH 17/34] fix properties in test --- .../Visual/Drawables/TestScenePiecewiseLinearToBSpline.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Framework.Tests/Visual/Drawables/TestScenePiecewiseLinearToBSpline.cs b/osu.Framework.Tests/Visual/Drawables/TestScenePiecewiseLinearToBSpline.cs index f2b001e1e3..b6a21c7459 100644 --- a/osu.Framework.Tests/Visual/Drawables/TestScenePiecewiseLinearToBSpline.cs +++ b/osu.Framework.Tests/Visual/Drawables/TestScenePiecewiseLinearToBSpline.cs @@ -193,7 +193,7 @@ public void UpdatePath() { if (!OptimizePath) return; - var controlPoints = PathApproximator.PiecewiseLinearToBSpline(inputPath, NumControlPoints, Degree, NumTestPoints, MaxIterations); + var controlPoints = PathApproximator.PiecewiseLinearToBSpline(inputPath, NumControlPoints, Degree, NumTestPoints, MaxIterations, LearningRate, B1, B2); Vertices = PathApproximator.BSplineToPiecewiseLinear(controlPoints.ToArray(), Degree); } } From fddf1beb8834b312806735ea2e9d6345718d9a91 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 22 Nov 2023 01:12:02 +0100 Subject: [PATCH 18/34] SIMD optimized matmul --- osu.Framework/Utils/PathApproximator.cs | 73 ++++++++++++++----------- osu.Framework/osu.Framework.csproj | 1 + 2 files changed, 42 insertions(+), 32 deletions(-) diff --git a/osu.Framework/Utils/PathApproximator.cs b/osu.Framework/Utils/PathApproximator.cs index 740827394c..2276cc59ef 100644 --- a/osu.Framework/Utils/PathApproximator.cs +++ b/osu.Framework/Utils/PathApproximator.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Numerics.Tensors; using osu.Framework.Graphics.Primitives; using osuTK; @@ -334,23 +335,33 @@ private static List piecewiseLinearToSpline(ReadOnlySpan input int interpolatorResolution = 100, List? initialControlPoints = null) { - // Generate Bezier weight matrix + // Generate transpose weight matrix int numControlPoints = weights.GetLength(1); int numTestPoints = weights.GetLength(0); + float[,] weightsTranspose = new float[numControlPoints, numTestPoints]; + + for (int i = 0; i < numControlPoints; i++) + { + for (int j = 0; j < numTestPoints; j++) + { + weightsTranspose[i, j] = weights[j, i]; + } + } + // Create efficient interpolation on the input path var interpolator = new Interpolator(inputPath, interpolatorResolution); // Initialize control points - float[,] labels = new float[numTestPoints, 2]; - float[,] controlPoints = new float[numControlPoints, 2]; + float[,] labels = new float[2, numTestPoints]; + float[,] controlPoints = new float[2, numControlPoints]; if (initialControlPoints is not null) { for (int i = 0; i < numControlPoints; i++) { - controlPoints[i, 0] = initialControlPoints[i].X; - controlPoints[i, 1] = initialControlPoints[i].Y; + controlPoints[0, i] = initialControlPoints[i].X; + controlPoints[1, i] = initialControlPoints[i].Y; } } else @@ -358,24 +369,24 @@ private static List piecewiseLinearToSpline(ReadOnlySpan input interpolator.Interpolate(linspace(0, 1, numControlPoints), controlPoints); // Initialize Adam optimizer variables - float[,] m = new float[numControlPoints, 2]; - float[,] v = new float[numControlPoints, 2]; - float[,] learnableMask = new float[numControlPoints, 2]; + float[,] m = new float[2, numControlPoints]; + float[,] v = new float[2, numControlPoints]; + float[,] learnableMask = new float[2, numControlPoints]; for (int i = 1; i < numControlPoints - 1; i++) { - learnableMask[i, 0] = 1; - learnableMask[i, 1] = 1; + learnableMask[0, i] = 1; + learnableMask[1, i] = 1; } // Initialize intermediate variables - float[,] points = new float[numTestPoints, 2]; - float[,] grad = new float[numControlPoints, 2]; + float[,] points = new float[2, numTestPoints]; + float[,] grad = new float[2, numControlPoints]; float[] distanceDistribution = new float[numTestPoints]; for (int step = 0; step < maxIterations; step++) { - matmul(weights, controlPoints, points); + matmul(controlPoints, weights, points); // Update labels to shift the distance distribution between points if (step % 11 == 0) @@ -386,7 +397,7 @@ private static List piecewiseLinearToSpline(ReadOnlySpan input // Calculate the gradient on the control points matDiff(labels, points, points); - matmulTranspose(weights, points, grad); + matmul(points, weightsTranspose, grad); matScale(grad, -1f / numControlPoints, grad); // Apply learnable mask to prevent moving the endpoints @@ -404,7 +415,7 @@ private static List piecewiseLinearToSpline(ReadOnlySpan input for (int i = 0; i < numControlPoints; i++) { - result.Add(new Vector2(controlPoints[i, 0], controlPoints[i, 1])); + result.Add(new Vector2(controlPoints[0, i], controlPoints[1, i])); } return result; @@ -507,24 +518,22 @@ private static void matDiff(float[,] mat1, float[,] mat2, float[,] result) } } - private static void matmul(float[,] mat1, float[,] mat2, float[,] result) + private static unsafe void matmul(float[,] mat1, float[,] mat2, float[,] result) { int m = mat1.GetLength(0); - int n = mat2.GetLength(1); + int n = mat2.GetLength(0); int p = mat1.GetLength(1); for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { - float sum = 0; - - for (int k = 0; k < p; k++) + fixed (float* mat1f = mat1, mat2f = mat2) { - sum += mat1[i, k] * mat2[k, j]; + var span1 = new Span(mat1f + i * p, p); + var span2 = new Span(mat2f + j * p, p); + result[i, j] = TensorPrimitives.Dot(span1, span2); } - - result[i, j] = sum; } } } @@ -543,13 +552,13 @@ private static float[] linspace(float start, float end, int count) private static void getDistanceDistribution(float[,] points, float[] result) { - int m = points.GetLength(0); + int m = points.GetLength(1); float accumulator = 0; result[0] = 0; for (int i = 1; i < m; i++) { - float dist = MathF.Sqrt(MathF.Pow(points[i, 0] - points[i - 1, 0], 2) + MathF.Pow(points[i, 1] - points[i - 1, 1], 2)); + float dist = MathF.Sqrt(MathF.Pow(points[0, i] - points[0, i - 1], 2) + MathF.Pow(points[1, i] - points[1, i - 1], 2)); accumulator += dist; result[i] = accumulator; } @@ -567,12 +576,12 @@ private class Interpolator public Interpolator(ReadOnlySpan inputPath, int resolution = 1000) { - float[,] arr = new float[inputPath.Length, 2]; + float[,] arr = new float[2, inputPath.Length]; for (int i = 0; i < inputPath.Length; i++) { - arr[i, 0] = inputPath[i].X; - arr[i, 1] = inputPath[i].Y; + arr[0, i] = inputPath[i].X; + arr[1, i] = inputPath[i].Y; } float[] dist = new float[inputPath.Length]; @@ -596,8 +605,8 @@ public Interpolator(ReadOnlySpan inputPath, int resolution = 1000) if (float.IsNaN(t)) t = 0; - ys[i, 0] = t * arr[prev, 0] + (1 - t) * arr[current, 0]; - ys[i, 1] = t * arr[prev, 1] + (1 - t) * arr[current, 1]; + ys[i, 0] = t * arr[0, prev] + (1 - t) * arr[0, current]; + ys[i, 1] = t * arr[1, prev] + (1 - t) * arr[1, current]; } } @@ -614,8 +623,8 @@ public void Interpolate(float[] x, float[,] result) float t = idx - idxBelow; - result[i, 0] = t * ys[idxAbove, 0] + (1 - t) * ys[idxBelow, 0]; - result[i, 1] = t * ys[idxAbove, 1] + (1 - t) * ys[idxBelow, 1]; + result[0, i] = t * ys[idxAbove, 0] + (1 - t) * ys[idxBelow, 0]; + result[1, i] = t * ys[idxAbove, 1] + (1 - t) * ys[idxBelow, 1]; } } } diff --git a/osu.Framework/osu.Framework.csproj b/osu.Framework/osu.Framework.csproj index 3ebc16d4f8..efbc45d207 100644 --- a/osu.Framework/osu.Framework.csproj +++ b/osu.Framework/osu.Framework.csproj @@ -52,5 +52,6 @@ + From 67db61bd9ff583b491fa022e44888c5d7a324305 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 22 Nov 2023 01:12:22 +0100 Subject: [PATCH 19/34] remove matmulTranspose --- osu.Framework/Utils/PathApproximator.cs | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/osu.Framework/Utils/PathApproximator.cs b/osu.Framework/Utils/PathApproximator.cs index 2276cc59ef..35f80adbdd 100644 --- a/osu.Framework/Utils/PathApproximator.cs +++ b/osu.Framework/Utils/PathApproximator.cs @@ -482,28 +482,6 @@ private static void matScale(float[,] mat, float scalar, float[,] result) } } - private static void matmulTranspose(float[,] mat1, float[,] mat2, float[,] result) - { - int m = mat1.GetLength(1); - int n = mat2.GetLength(1); - int p = mat1.GetLength(0); - - for (int i = 0; i < m; i++) - { - for (int j = 0; j < n; j++) - { - float sum = 0; - - for (int k = 0; k < p; k++) - { - sum += mat1[k, i] * mat2[k, j]; - } - - result[i, j] = sum; - } - } - } - private static void matDiff(float[,] mat1, float[,] mat2, float[,] result) { int m = mat1.GetLength(0); From 4ce570e2fa4b76a4a877271f203b67be04aedbb1 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 22 Nov 2023 02:17:23 +0100 Subject: [PATCH 20/34] SIMD on other matrix ops --- osu.Framework/Utils/PathApproximator.cs | 48 +++++++++++++++---------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/osu.Framework/Utils/PathApproximator.cs b/osu.Framework/Utils/PathApproximator.cs index 35f80adbdd..a58ff42ce3 100644 --- a/osu.Framework/Utils/PathApproximator.cs +++ b/osu.Framework/Utils/PathApproximator.cs @@ -440,58 +440,70 @@ private static void adamUpdate(float[,] parameters, float[,] m, float[,] v, int } } - private static void matLerp(float[,] mat1, float[,] mat2, float t, float[,] result) + private static unsafe void matLerp(float[,] mat1, float[,] mat2, float t, float[,] result) { int m = mat1.GetLength(0); int n = mat1.GetLength(1); for (int i = 0; i < m; i++) { - for (int j = 0; j < n; j++) + fixed (float* mat1P = mat1, mat2P = mat2, resultP = result) { - result[i, j] = mat1[i, j] * (1 - t) + mat2[i, j] * t; + var span1 = new Span(mat1P + i * n, n); + var span2 = new Span(mat2P + i * n, n); + var spanR = new Span(resultP + i * n, n); + TensorPrimitives.Multiply(span2, t, spanR); + TensorPrimitives.MultiplyAdd(span1, 1 - t, spanR, spanR); } } } - private static void matProduct(float[,] mat1, float[,] mat2, float[,] result) + private static unsafe void matProduct(float[,] mat1, float[,] mat2, float[,] result) { int m = mat1.GetLength(0); int n = mat1.GetLength(1); for (int i = 0; i < m; i++) { - for (int j = 0; j < n; j++) + fixed (float* mat1P = mat1, mat2P = mat2, resultP = result) { - result[i, j] = mat1[i, j] * mat2[i, j]; + var span1 = new Span(mat1P + i * n, n); + var span2 = new Span(mat2P + i * n, n); + var spanR = new Span(resultP + i * n, n); + TensorPrimitives.Multiply(span1, span2, spanR); } } } - private static void matScale(float[,] mat, float scalar, float[,] result) + private static unsafe void matScale(float[,] mat, float scalar, float[,] result) { int m = mat.GetLength(0); int n = mat.GetLength(1); for (int i = 0; i < m; i++) { - for (int j = 0; j < n; j++) + fixed (float* matP = mat, resultP = result) { - result[i, j] = mat[i, j] * scalar; + var span1 = new Span(matP + i * n, n); + var spanR = new Span(resultP + i * n, n); + TensorPrimitives.Multiply(span1, scalar, spanR); } } } - private static void matDiff(float[,] mat1, float[,] mat2, float[,] result) + private static unsafe void matDiff(float[,] mat1, float[,] mat2, float[,] result) { int m = mat1.GetLength(0); int n = mat1.GetLength(1); for (int i = 0; i < m; i++) { - for (int j = 0; j < n; j++) + fixed (float* mat1P = mat1, mat2P = mat2, resultP = result) { - result[i, j] = mat1[i, j] - mat2[i, j]; + var span1 = new Span(mat1P + i * n, n); + var span2 = new Span(mat2P + i * n, n); + var spanR = new Span(resultP + i * n, n); + TensorPrimitives.Subtract(span1, span2, spanR); } } } @@ -506,10 +518,10 @@ private static unsafe void matmul(float[,] mat1, float[,] mat2, float[,] result) { for (int j = 0; j < n; j++) { - fixed (float* mat1f = mat1, mat2f = mat2) + fixed (float* mat1P = mat1, mat2P = mat2) { - var span1 = new Span(mat1f + i * p, p); - var span2 = new Span(mat2f + j * p, p); + var span1 = new Span(mat1P + i * p, p); + var span2 = new Span(mat2P + j * p, p); result[i, j] = TensorPrimitives.Dot(span1, span2); } } @@ -541,10 +553,8 @@ private static void getDistanceDistribution(float[,] points, float[] result) result[i] = accumulator; } - for (int i = 0; i < m; i++) - { - result[i] /= accumulator; - } + var spanR = result.AsSpan(); + TensorPrimitives.Divide(spanR, accumulator, spanR); } private class Interpolator From 69f97998d97ac2f2ceac778acef653e27ca0dce5 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 22 Nov 2023 02:40:23 +0100 Subject: [PATCH 21/34] update comments --- osu.Framework/Utils/PathApproximator.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Framework/Utils/PathApproximator.cs b/osu.Framework/Utils/PathApproximator.cs index a58ff42ce3..be791ea48a 100644 --- a/osu.Framework/Utils/PathApproximator.cs +++ b/osu.Framework/Utils/PathApproximator.cs @@ -335,10 +335,10 @@ private static List piecewiseLinearToSpline(ReadOnlySpan input int interpolatorResolution = 100, List? initialControlPoints = null) { - // Generate transpose weight matrix int numControlPoints = weights.GetLength(1); int numTestPoints = weights.GetLength(0); + // Generate transpose weight matrix float[,] weightsTranspose = new float[numControlPoints, numTestPoints]; for (int i = 0; i < numControlPoints; i++) @@ -410,7 +410,7 @@ private static List piecewiseLinearToSpline(ReadOnlySpan input adamUpdate(controlPoints, m, v, step, learningRate, b1, b2); } - // Convert the resulting control points NDArray + // Convert the resulting control points array var result = new List(numControlPoints); for (int i = 0; i < numControlPoints; i++) @@ -626,7 +626,7 @@ public void Interpolate(float[] x, float[,] result) /// Matrix array of B-spline basis function values. private static float[,] generateBSplineWeights(int numControlPoints, int numTestPoints, int degree) { - // Calculate the basis function values using a modified vectorized De Boor's algorithm + // Calculate the basis function values using a modified De Boor's algorithm float[] x = linspace(0, 1, numTestPoints); float[] knots = new float[numControlPoints + degree + 1]; From 1399a1ee95c9768731d7d5590504005b7a18ac6c Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 22 Nov 2023 02:40:41 +0100 Subject: [PATCH 22/34] simplify vectorizations --- osu.Framework/Utils/PathApproximator.cs | 64 ++++++++----------------- 1 file changed, 20 insertions(+), 44 deletions(-) diff --git a/osu.Framework/Utils/PathApproximator.cs b/osu.Framework/Utils/PathApproximator.cs index be791ea48a..cd361b6d03 100644 --- a/osu.Framework/Utils/PathApproximator.cs +++ b/osu.Framework/Utils/PathApproximator.cs @@ -442,69 +442,45 @@ private static void adamUpdate(float[,] parameters, float[,] m, float[,] v, int private static unsafe void matLerp(float[,] mat1, float[,] mat2, float t, float[,] result) { - int m = mat1.GetLength(0); - int n = mat1.GetLength(1); - - for (int i = 0; i < m; i++) + fixed (float* mat1P = mat1, mat2P = mat2, resultP = result) { - fixed (float* mat1P = mat1, mat2P = mat2, resultP = result) - { - var span1 = new Span(mat1P + i * n, n); - var span2 = new Span(mat2P + i * n, n); - var spanR = new Span(resultP + i * n, n); - TensorPrimitives.Multiply(span2, t, spanR); - TensorPrimitives.MultiplyAdd(span1, 1 - t, spanR, spanR); - } + var span1 = new Span(mat1P, mat1.Length); + var span2 = new Span(mat2P, mat2.Length); + var spanR = new Span(resultP, result.Length); + TensorPrimitives.Multiply(span2, t, spanR); + TensorPrimitives.MultiplyAdd(span1, 1 - t, spanR, spanR); } } private static unsafe void matProduct(float[,] mat1, float[,] mat2, float[,] result) { - int m = mat1.GetLength(0); - int n = mat1.GetLength(1); - - for (int i = 0; i < m; i++) + fixed (float* mat1P = mat1, mat2P = mat2, resultP = result) { - fixed (float* mat1P = mat1, mat2P = mat2, resultP = result) - { - var span1 = new Span(mat1P + i * n, n); - var span2 = new Span(mat2P + i * n, n); - var spanR = new Span(resultP + i * n, n); - TensorPrimitives.Multiply(span1, span2, spanR); - } + var span1 = new Span(mat1P, mat1.Length); + var span2 = new Span(mat2P, mat2.Length); + var spanR = new Span(resultP, result.Length); + TensorPrimitives.Multiply(span1, span2, spanR); } } private static unsafe void matScale(float[,] mat, float scalar, float[,] result) { - int m = mat.GetLength(0); - int n = mat.GetLength(1); - - for (int i = 0; i < m; i++) + fixed (float* matP = mat, resultP = result) { - fixed (float* matP = mat, resultP = result) - { - var span1 = new Span(matP + i * n, n); - var spanR = new Span(resultP + i * n, n); - TensorPrimitives.Multiply(span1, scalar, spanR); - } + var span1 = new Span(matP, mat.Length); + var spanR = new Span(resultP, result.Length); + TensorPrimitives.Multiply(span1, scalar, spanR); } } private static unsafe void matDiff(float[,] mat1, float[,] mat2, float[,] result) { - int m = mat1.GetLength(0); - int n = mat1.GetLength(1); - - for (int i = 0; i < m; i++) + fixed (float* mat1P = mat1, mat2P = mat2, resultP = result) { - fixed (float* mat1P = mat1, mat2P = mat2, resultP = result) - { - var span1 = new Span(mat1P + i * n, n); - var span2 = new Span(mat2P + i * n, n); - var spanR = new Span(resultP + i * n, n); - TensorPrimitives.Subtract(span1, span2, spanR); - } + var span1 = new Span(mat1P, mat1.Length); + var span2 = new Span(mat2P, mat2.Length); + var spanR = new Span(resultP, result.Length); + TensorPrimitives.Subtract(span1, span2, spanR); } } From 278251231e32ac018fb25f00609761646034952c Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 24 Nov 2023 10:20:52 +0100 Subject: [PATCH 23/34] Add useful comments --- osu.Framework/Utils/PathApproximator.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Framework/Utils/PathApproximator.cs b/osu.Framework/Utils/PathApproximator.cs index cd361b6d03..9387f57592 100644 --- a/osu.Framework/Utils/PathApproximator.cs +++ b/osu.Framework/Utils/PathApproximator.cs @@ -303,8 +303,8 @@ public static List PiecewiseLinearToBezier(ReadOnlySpan inputP int numTestPoints = 100, int maxIterations = 100, float learningRate = 8f, - float b1 = 0.9f, - float b2 = 0.92f, + float b1 = 0.8f, + float b2 = 0.99f, int interpolatorResolution = 100, List? initialControlPoints = null) { @@ -330,8 +330,8 @@ private static List piecewiseLinearToSpline(ReadOnlySpan input float[,] weights, int maxIterations = 100, float learningRate = 8f, - float b1 = 0.9f, - float b2 = 0.92f, + float b1 = 0.8f, + float b2 = 0.99f, int interpolatorResolution = 100, List? initialControlPoints = null) { @@ -440,6 +440,7 @@ private static void adamUpdate(float[,] parameters, float[,] m, float[,] v, int } } + // mat1 can not be the same array as result, or it will not work correctly private static unsafe void matLerp(float[,] mat1, float[,] mat2, float t, float[,] result) { fixed (float* mat1P = mat1, mat2P = mat2, resultP = result) @@ -484,6 +485,8 @@ private static unsafe void matDiff(float[,] mat1, float[,] mat2, float[,] result } } + // This matmul operation is not standard because it computes (m, p) * (n, p) -> (m, n) + // This is because the memory for the reduced dimension must be contiguous private static unsafe void matmul(float[,] mat1, float[,] mat2, float[,] result) { int m = mat1.GetLength(0); @@ -630,6 +633,9 @@ public void Interpolate(float[] x, float[,] result) { for (int i = 0; i < numTestPoints; i++) { + // This code multiplies the previous order by equal length arrays of alphas and betas, + // then shifts the alpha array by one index, and adds the results, resulting in one extra length. + // nextOrder = (prevOrder * alphas).shiftRight() + (prevOrder * betas) float prevAlpha = 0; for (int j = 0; j < numControlPoints - degree + q - 1; j++) From fe9eb50dd1574af8d89d4c72b1616f1f509d8b59 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 24 Nov 2023 16:20:11 +0100 Subject: [PATCH 24/34] Revert "code quality fixes" This reverts commit c7745ae0ffabe617ef377f1a27c0d16f414d1b2c. --- osu.Framework/Utils/IncrementalBSplineBuilder.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Framework/Utils/IncrementalBSplineBuilder.cs b/osu.Framework/Utils/IncrementalBSplineBuilder.cs index 2718106c44..119a055f8e 100644 --- a/osu.Framework/Utils/IncrementalBSplineBuilder.cs +++ b/osu.Framework/Utils/IncrementalBSplineBuilder.cs @@ -286,7 +286,8 @@ private void regenerateApproximatedPathControlPoints() Debug.Assert(vertices.Count == distances.Count + 1); var cornerTs = detectCorners(vertices, distances); - var cps = new List { vertices[0] }; + var cps = new List(); + cps.Add(vertices[0]); // Populate each segment between corners with control points that have density proportional to the // product of Tolerance and curvature. From 8ab1d6c2d97857a54d9fd9eafd8591fa8fbfc148 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 24 Nov 2023 16:20:25 +0100 Subject: [PATCH 25/34] Revert "increase max iterations" This reverts commit 67243055867334c07c5fae88d48641e60ac95408. --- osu.Framework/Utils/IncrementalBSplineBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Framework/Utils/IncrementalBSplineBuilder.cs b/osu.Framework/Utils/IncrementalBSplineBuilder.cs index 119a055f8e..f4b0fbea75 100644 --- a/osu.Framework/Utils/IncrementalBSplineBuilder.cs +++ b/osu.Framework/Utils/IncrementalBSplineBuilder.cs @@ -353,7 +353,7 @@ private void regenerateApproximatedPathControlPoints() cps.AddRange(Enumerable.Repeat(c1, degree)); } - controlPoints.Value = PathApproximator.PiecewiseLinearToBSpline(vertices.ToArray(), cps.Count, degree, maxIterations: 200, initialControlPoints: cps); + controlPoints.Value = PathApproximator.PiecewiseLinearToBSpline(vertices.ToArray(), cps.Count, degree, initialControlPoints: cps); } private void redrawApproximatedPath() From 30daa087335e154c7dd55ee6c9da6f83e3459944 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 24 Nov 2023 16:20:31 +0100 Subject: [PATCH 26/34] Revert "use smooth path for shape" This reverts commit c112ed08afd77227705291397cf5f315b3762675. --- osu.Framework/Utils/IncrementalBSplineBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Framework/Utils/IncrementalBSplineBuilder.cs b/osu.Framework/Utils/IncrementalBSplineBuilder.cs index f4b0fbea75..6095805d63 100644 --- a/osu.Framework/Utils/IncrementalBSplineBuilder.cs +++ b/osu.Framework/Utils/IncrementalBSplineBuilder.cs @@ -353,7 +353,7 @@ private void regenerateApproximatedPathControlPoints() cps.AddRange(Enumerable.Repeat(c1, degree)); } - controlPoints.Value = PathApproximator.PiecewiseLinearToBSpline(vertices.ToArray(), cps.Count, degree, initialControlPoints: cps); + controlPoints.Value = PathApproximator.PiecewiseLinearToBSpline(inputPath.ToArray(), cps.Count, degree, initialControlPoints: cps); } private void redrawApproximatedPath() From 936849434a0a5baf35bbec5bac72b379882e6e3b Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 24 Nov 2023 16:20:38 +0100 Subject: [PATCH 27/34] Revert "Add naive use of optimizer to IncrementalBSplineBuilder" This reverts commit f847b3f67634e74038cde3a09dc4b504be0d61c9. --- osu.Framework/Utils/IncrementalBSplineBuilder.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Framework/Utils/IncrementalBSplineBuilder.cs b/osu.Framework/Utils/IncrementalBSplineBuilder.cs index 6095805d63..fbab87371b 100644 --- a/osu.Framework/Utils/IncrementalBSplineBuilder.cs +++ b/osu.Framework/Utils/IncrementalBSplineBuilder.cs @@ -283,10 +283,12 @@ private void regenerateApproximatedPathControlPoints() return; } + controlPoints.Value = new List(); + Debug.Assert(vertices.Count == distances.Count + 1); var cornerTs = detectCorners(vertices, distances); - var cps = new List(); + var cps = controlPoints.Value; cps.Add(vertices[0]); // Populate each segment between corners with control points that have density proportional to the @@ -352,8 +354,6 @@ private void regenerateApproximatedPathControlPoints() else cps.AddRange(Enumerable.Repeat(c1, degree)); } - - controlPoints.Value = PathApproximator.PiecewiseLinearToBSpline(inputPath.ToArray(), cps.Count, degree, initialControlPoints: cps); } private void redrawApproximatedPath() From 506c2ae0fc0a49f585f4485ce66730141fa64c4b Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sun, 3 Dec 2023 14:47:56 +0100 Subject: [PATCH 28/34] remove interpolator resolution arg --- osu.Framework/Utils/PathApproximator.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/osu.Framework/Utils/PathApproximator.cs b/osu.Framework/Utils/PathApproximator.cs index 9387f57592..4f45478507 100644 --- a/osu.Framework/Utils/PathApproximator.cs +++ b/osu.Framework/Utils/PathApproximator.cs @@ -305,12 +305,12 @@ public static List PiecewiseLinearToBezier(ReadOnlySpan inputP float learningRate = 8f, float b1 = 0.8f, float b2 = 0.99f, - int interpolatorResolution = 100, List? initialControlPoints = null) { - return piecewiseLinearToSpline(inputPath, generateBezierWeights(numControlPoints, numTestPoints), maxIterations, learningRate, b1, b2, interpolatorResolution, initialControlPoints); + return piecewiseLinearToSpline(inputPath, generateBezierWeights(numControlPoints, numTestPoints), maxIterations, learningRate, b1, b2, initialControlPoints); } + /// The polynomial order. public static List PiecewiseLinearToBSpline(ReadOnlySpan inputPath, int numControlPoints, int degree, @@ -319,11 +319,10 @@ public static List PiecewiseLinearToBSpline(ReadOnlySpan input float learningRate = 8f, float b1 = 0.8f, float b2 = 0.99f, - int interpolatorResolution = 100, List? initialControlPoints = null) { degree = Math.Min(degree, numControlPoints - 1); - return piecewiseLinearToSpline(inputPath, generateBSplineWeights(numControlPoints, numTestPoints, degree), maxIterations, learningRate, b1, b2, interpolatorResolution, initialControlPoints); + return piecewiseLinearToSpline(inputPath, generateBSplineWeights(numControlPoints, numTestPoints, degree), maxIterations, learningRate, b1, b2, initialControlPoints); } private static List piecewiseLinearToSpline(ReadOnlySpan inputPath, @@ -332,7 +331,6 @@ private static List piecewiseLinearToSpline(ReadOnlySpan input float learningRate = 8f, float b1 = 0.8f, float b2 = 0.99f, - int interpolatorResolution = 100, List? initialControlPoints = null) { int numControlPoints = weights.GetLength(1); @@ -350,7 +348,7 @@ private static List piecewiseLinearToSpline(ReadOnlySpan input } // Create efficient interpolation on the input path - var interpolator = new Interpolator(inputPath, interpolatorResolution); + var interpolator = new Interpolator(inputPath, numTestPoints); // Initialize control points float[,] labels = new float[2, numTestPoints]; From 5882173e2c87acfd1884c5777b43df3145ff96e3 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sun, 3 Dec 2023 18:59:34 +0100 Subject: [PATCH 29/34] add xmldoc and fix comment --- osu.Framework/Utils/PathApproximator.cs | 40 ++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/osu.Framework/Utils/PathApproximator.cs b/osu.Framework/Utils/PathApproximator.cs index 4f45478507..55832c2395 100644 --- a/osu.Framework/Utils/PathApproximator.cs +++ b/osu.Framework/Utils/PathApproximator.cs @@ -298,6 +298,18 @@ public static List LagrangePolynomialToPiecewiseLinear(ReadOnlySpan + /// Creates a bezier curve approximation from a piecewise-linear path. + /// + /// The piecewise-linear path to approximate. + /// The number of control points to use in the bezier approximation. + /// The number of points to evaluate the bezier path at for optimization, basically a resolution. + /// The number of optimization steps. + /// The rate of optimization. Larger values converge faster but can be unstable. + /// The B1 parameter for the Adam optimizer. Between 0 and 1. + /// The B2 parameter for the Adam optimizer. Between 0 and 1. + /// The initial bezier control points to use before optimization. The length of this list should be equal to . + /// A List of vectors representing the bezier control points. public static List PiecewiseLinearToBezier(ReadOnlySpan inputPath, int numControlPoints, int numTestPoints = 100, @@ -310,7 +322,19 @@ public static List PiecewiseLinearToBezier(ReadOnlySpan inputP return piecewiseLinearToSpline(inputPath, generateBezierWeights(numControlPoints, numTestPoints), maxIterations, learningRate, b1, b2, initialControlPoints); } + /// + /// Creates a B-spline approximation from a piecewise-linear path. + /// + /// The piecewise-linear path to approximate. + /// The number of control points to use in the B-spline approximation. /// The polynomial order. + /// The number of points to evaluate the B-spline path at for optimization, basically a resolution. + /// The number of optimization steps. + /// The rate of optimization. Larger values converge faster but can be unstable. + /// The B1 parameter for the Adam optimizer. Between 0 and 1. + /// The B2 parameter for the Adam optimizer. Between 0 and 1. + /// The initial B-spline control points to use before optimization. The length of this list should be equal to . + /// A List of vectors representing the B-spline control points. public static List PiecewiseLinearToBSpline(ReadOnlySpan inputPath, int numControlPoints, int degree, @@ -325,6 +349,18 @@ public static List PiecewiseLinearToBSpline(ReadOnlySpan input return piecewiseLinearToSpline(inputPath, generateBSplineWeights(numControlPoints, numTestPoints, degree), maxIterations, learningRate, b1, b2, initialControlPoints); } + /// + /// Creates an arbitrary spline approximation from a piecewise-linear path. + /// Works for any spline type where the interpolation is a linear combination of the control points. + /// + /// The piecewise-linear path to approximate. + /// A 2D matrix that contains the spline basis functions at multiple positions. The length of the first dimension is the number of test points, and the length of the second dimension is the number of control points. + /// The number of optimization steps. + /// The rate of optimization. Larger values converge faster but can be unstable. + /// The B1 parameter for the Adam optimizer. Between 0 and 1. + /// The B2 parameter for the Adam optimizer. Between 0 and 1. + /// The initial control points to use before optimization. The length of this list should be equal to the number of test points. + /// A List of vectors representing the spline control points. private static List piecewiseLinearToSpline(ReadOnlySpan inputPath, float[,] weights, int maxIterations = 100, @@ -603,7 +639,9 @@ public void Interpolate(float[] x, float[,] result) /// Matrix array of B-spline basis function values. private static float[,] generateBSplineWeights(int numControlPoints, int numTestPoints, int degree) { - // Calculate the basis function values using a modified De Boor's algorithm + // Calculate the basis function values using the Cox-de Boor recursion formula + + // Generate an open uniform knot vector from 0 to 1 float[] x = linspace(0, 1, numTestPoints); float[] knots = new float[numControlPoints + degree + 1]; From 23dc4e77506d4da7b5226807a474635f400ab23d Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sun, 3 Dec 2023 19:14:53 +0100 Subject: [PATCH 30/34] add checks for weight generating func arguments --- osu.Framework/Utils/PathApproximator.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/osu.Framework/Utils/PathApproximator.cs b/osu.Framework/Utils/PathApproximator.cs index 55832c2395..882dd50f39 100644 --- a/osu.Framework/Utils/PathApproximator.cs +++ b/osu.Framework/Utils/PathApproximator.cs @@ -640,6 +640,14 @@ public void Interpolate(float[] x, float[,] result) private static float[,] generateBSplineWeights(int numControlPoints, int numTestPoints, int degree) { // Calculate the basis function values using the Cox-de Boor recursion formula + if (numControlPoints < 2) + throw new ArgumentOutOfRangeException(nameof(numControlPoints), $"{nameof(numControlPoints)} must be >=2 but was {numControlPoints}."); + + if (numTestPoints < 2) + throw new ArgumentOutOfRangeException(nameof(numTestPoints), $"{nameof(numTestPoints)} must be >=2 but was {numTestPoints}."); + + if (degree < 0 || degree >= numControlPoints) + throw new ArgumentOutOfRangeException(nameof(degree), $"{nameof(degree)} must be >=0 and <{nameof(numControlPoints)} but was {degree}."); // Generate an open uniform knot vector from 0 to 1 float[] x = linspace(0, 1, numTestPoints); @@ -692,6 +700,12 @@ public void Interpolate(float[] x, float[,] result) private static float[,] generateBezierWeights(int numControlPoints, int numTestPoints) { + if (numControlPoints < 2) + throw new ArgumentOutOfRangeException(nameof(numControlPoints), $"{nameof(numControlPoints)} must be >=2 but was {numControlPoints}."); + + if (numTestPoints < 2) + throw new ArgumentOutOfRangeException(nameof(numTestPoints), $"{nameof(numTestPoints)} must be >=2 but was {numTestPoints}."); + long[] coefficients = binomialCoefficients(numControlPoints); float[,] p = new float[numTestPoints, numControlPoints]; From 5e075ad298652603054be8117430893d0d66433a Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sun, 3 Dec 2023 19:15:05 +0100 Subject: [PATCH 31/34] move comment --- osu.Framework/Utils/PathApproximator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Framework/Utils/PathApproximator.cs b/osu.Framework/Utils/PathApproximator.cs index 882dd50f39..490a04a230 100644 --- a/osu.Framework/Utils/PathApproximator.cs +++ b/osu.Framework/Utils/PathApproximator.cs @@ -639,7 +639,6 @@ public void Interpolate(float[] x, float[,] result) /// Matrix array of B-spline basis function values. private static float[,] generateBSplineWeights(int numControlPoints, int numTestPoints, int degree) { - // Calculate the basis function values using the Cox-de Boor recursion formula if (numControlPoints < 2) throw new ArgumentOutOfRangeException(nameof(numControlPoints), $"{nameof(numControlPoints)} must be >=2 but was {numControlPoints}."); @@ -649,6 +648,7 @@ public void Interpolate(float[] x, float[,] result) if (degree < 0 || degree >= numControlPoints) throw new ArgumentOutOfRangeException(nameof(degree), $"{nameof(degree)} must be >=0 and <{nameof(numControlPoints)} but was {degree}."); + // Calculate the basis function values using the Cox-de Boor recursion formula // Generate an open uniform knot vector from 0 to 1 float[] x = linspace(0, 1, numTestPoints); float[] knots = new float[numControlPoints + degree + 1]; From 30a924f38be492910556f13d67efed01c1a2cd21 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sun, 3 Dec 2023 19:23:27 +0100 Subject: [PATCH 32/34] Changed binomialCoefficients to return n+1 terms --- osu.Framework/Utils/PathApproximator.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/osu.Framework/Utils/PathApproximator.cs b/osu.Framework/Utils/PathApproximator.cs index 490a04a230..26ef994a78 100644 --- a/osu.Framework/Utils/PathApproximator.cs +++ b/osu.Framework/Utils/PathApproximator.cs @@ -706,7 +706,7 @@ public void Interpolate(float[] x, float[,] result) if (numTestPoints < 2) throw new ArgumentOutOfRangeException(nameof(numTestPoints), $"{nameof(numTestPoints)} must be >=2 but was {numTestPoints}."); - long[] coefficients = binomialCoefficients(numControlPoints); + long[] coefficients = binomialCoefficients(numControlPoints - 1); float[,] p = new float[numTestPoints, numControlPoints]; for (int i = 0; i < numTestPoints; i++) @@ -733,19 +733,23 @@ public void Interpolate(float[] x, float[,] result) return result; } + /// + /// Computes an array with all binomial coefficients from 0 to n inclusive. + /// + /// n+1 length array with the binomial coefficients. private static long[] binomialCoefficients(int n) { - long[] coefficients = new long[n]; + long[] coefficients = new long[n + 1]; coefficients[0] = 1; - for (int i = 1; i < (n + 1) / 2; i++) + for (int i = 1; i < (n + 2) / 2; i++) { - coefficients[i] = coefficients[i - 1] * (n - i) / i; + coefficients[i] = coefficients[i - 1] * (n + 1 - i) / i; } - for (int i = n - 1; i > (n - 1) / 2; i--) + for (int i = n; i > n / 2; i--) { - coefficients[i] = coefficients[n - i - 1]; + coefficients[i] = coefficients[n - i]; } return coefficients; From 46c947161565698a222ed83076d1fc60876301c2 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sun, 3 Dec 2023 19:28:07 +0100 Subject: [PATCH 33/34] Add xmldoc to getDistanceDistribution --- osu.Framework/Utils/PathApproximator.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Framework/Utils/PathApproximator.cs b/osu.Framework/Utils/PathApproximator.cs index 26ef994a78..dcfb9eb38f 100644 --- a/osu.Framework/Utils/PathApproximator.cs +++ b/osu.Framework/Utils/PathApproximator.cs @@ -553,6 +553,11 @@ private static float[] linspace(float start, float end, int count) return result; } + /// + /// Calculates a normalized cumulative distribution for the Euclidean distance between points on a piecewise-linear path. + /// + /// (2, n) shape array which represents the points of the piecewise-linear path. + /// n-length array to write the result to. private static void getDistanceDistribution(float[,] points, float[] result) { int m = points.GetLength(1); From 0f8d3cc9c44877625bb889e1d05c44aaccafb51a Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sun, 3 Dec 2023 19:31:33 +0100 Subject: [PATCH 34/34] Add argument check for matLerp --- osu.Framework/Utils/PathApproximator.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Framework/Utils/PathApproximator.cs b/osu.Framework/Utils/PathApproximator.cs index dcfb9eb38f..1228f5c75d 100644 --- a/osu.Framework/Utils/PathApproximator.cs +++ b/osu.Framework/Utils/PathApproximator.cs @@ -474,9 +474,12 @@ private static void adamUpdate(float[,] parameters, float[,] m, float[,] v, int } } - // mat1 can not be the same array as result, or it will not work correctly private static unsafe void matLerp(float[,] mat1, float[,] mat2, float t, float[,] result) { + // mat1 can not be the same array as result, or it will not work correctly + if (ReferenceEquals(mat1, result)) + throw new ArgumentException($"{nameof(mat1)} can not be the same array as {nameof(result)}."); + fixed (float* mat1P = mat1, mat2P = mat2, resultP = result) { var span1 = new Span(mat1P, mat1.Length);