diff --git a/orx-gradient-descent/README.md b/orx-gradient-descent/README.md new file mode 100644 index 00000000..37c00b2c --- /dev/null +++ b/orx-gradient-descent/README.md @@ -0,0 +1,36 @@ +# orx-gradient-descent + +A gradient descent based minimizer that is incredibly +easy to use. + +## Usage + +```kotlin +// define a model +class Model { + var x = 0.0 + var y = 0.0 +} + +val model = Model() +minimizeModel(model) { m -> + (m.x-4.0)*(m.x-4.0) + (m.y-3.0)*(m.y-3.0) +} + +// model.x is close to 4 and model y is close to 3 at this point +``` + +## Data binding + +Currently we support minimizing model classes that contain +`Double`, `Vector2`, `Vector3` and `Vector4` typed properties, +other types are silently ignored. + +An example of a supported model: +```kotlin +class Model { + var x = 0.0 + var y = 0.0 + var v2 = Vector2.ZERO +} +``` \ No newline at end of file diff --git a/orx-gradient-descent/src/main/kotlin/GradientDescent.kt b/orx-gradient-descent/src/main/kotlin/GradientDescent.kt index 58574959..2a447f2b 100644 --- a/orx-gradient-descent/src/main/kotlin/GradientDescent.kt +++ b/orx-gradient-descent/src/main/kotlin/GradientDescent.kt @@ -96,7 +96,7 @@ fun dot(x: Array, y: DoubleArray): DoubleArray = DoubleArray(x.size class MinimizationResult(val solution: DoubleArray, val value: Double, val gradient: DoubleArray, val inverseHessian: Array, val iterations: Int) -fun minimize(_x0: DoubleArray, endOnLineSearch: Boolean = true, tol: Double = 1e-8, maxIterations: Int = 1000, f: (DoubleArray) -> Double): MinimizationResult { +fun minimize(_x0: DoubleArray, endOnLineSearch: Boolean = false, tol: Double = 1e-8, maxIterations: Int = 1000, f: (DoubleArray) -> Double): MinimizationResult { val grad = { a: DoubleArray -> gradient(a, f) } var x0 = _x0.copyOf() var g0 = grad(x0) @@ -108,8 +108,8 @@ fun minimize(_x0: DoubleArray, endOnLineSearch: Boolean = true, tol: Double = 1e while (iteration < maxIterations) { require(g0.all { it == it && it != Double.POSITIVE_INFINITY && it != Double.NEGATIVE_INFINITY }) val pstep = dot(H1, g0) - require(pstep.all { it == it }) { "pstep contains NaNs"} - require(pstep.all { it != Double.POSITIVE_INFINITY && it != Double.NEGATIVE_INFINITY }) { "pstep contains infs" } + require(pstep.all { it == it }) { "pstep contains NaNs" } + require(pstep.all { it != Double.POSITIVE_INFINITY && it != Double.NEGATIVE_INFINITY }) { "pstep contains infs" } val step = neg(pstep) val nstep = norm2(step) @@ -127,7 +127,7 @@ fun minimize(_x0: DoubleArray, endOnLineSearch: Boolean = true, tol: Double = 1e x1 = add(x0, s) f1 = f(x1) - require(f1 == f1) { "f1 is NaN"} + require(f1 == f1) { "f1 is NaN" } if (!(f1 - f0 >= 0.1 * t * df0)) { break } @@ -144,7 +144,7 @@ fun minimize(_x0: DoubleArray, endOnLineSearch: Boolean = true, tol: Double = 1e require(g1.all { it == it }) val y = sub(g1, g0) val ys = dot(y, s) - if (ys==0.0) { + if (ys == 0.0) { break } val Hy = dot(H1, y) @@ -172,3 +172,12 @@ fun minimize(_x0: DoubleArray, endOnLineSearch: Boolean = true, tol: Double = 1e return MinimizationResult(x0, f0, g0, H1, iteration) } + +fun minimizeModel(model: T, endOnLineSearch: Boolean = false, tol: Double = 1e-8, maxIterations: Int = 1000, function: (T) -> Double) { + val doubles = modelToArray(model) + val solution = minimize(doubles, endOnLineSearch, tol, maxIterations) { + arrayToModel(it, model) + function(model) + } + arrayToModel(solution.solution, model) +} \ No newline at end of file diff --git a/orx-gradient-descent/src/test/kotlin/TestMinimizeModel.kt b/orx-gradient-descent/src/test/kotlin/TestMinimizeModel.kt new file mode 100644 index 00000000..47a885fc --- /dev/null +++ b/orx-gradient-descent/src/test/kotlin/TestMinimizeModel.kt @@ -0,0 +1,34 @@ +import org.amshove.kluent.shouldBeNear +import org.openrndr.extra.gradientdescent.minimizeModel +import org.openrndr.math.Vector2 +import org.spekframework.spek2.Spek +import org.spekframework.spek2.style.specification.describe + +object TestMinimizeModel : Spek({ + describe("a model") { + val m = object { + var x = 0.0 + var y = 0.0 + } + it("can be minimized") { + minimizeModel(m) { m-> + (m.x - 4.0) * (m.x - 4.0) + (m.y - 3.0) * (m.y - 3.0) + } + m.x.shouldBeNear(4.0, 0.01) + m.y.shouldBeNear(3.0, 0.01) + } + } + + describe("a model with a Vector2 property") { + val m = object { + var position = Vector2.ZERO + } + it("can be minimized") { + minimizeModel(m) { m-> + (m.position.x - 4.0) * (m.position.x - 4.0) + (m.position.y - 3.0) * (m.position.y - 3.0) + } + m.position.x.shouldBeNear(4.0, 0.01) + m.position.y.shouldBeNear(3.0, 0.01) + } + } +}) \ No newline at end of file