Skip to content

LeastSquaresAndFMChirpSmokeTest.kt

src/jvmMain/kotlin/afield/examples/stdlib/LeastSquaresAndFMChirpSmokeTest.kt · 101 lines

package afield.examples.stdlib

import afield.graphics.vulkan.util.FMChirpCurves
import afield.math.optimization.LeastSquaresOptions
import afield.math.optimization.leastSquares
import kotlin.math.abs
import kotlin.math.sin

object LeastSquaresAndFMChirpSmokeTest

FIELD_TEST id: stdlib.least-squares-fmchirp.smoke runner: jvm-main entrypoint: afield.examples.stdlib.LeastSquaresAndFMChirpSmokeTest tags: [smoke, stdlib, numerics, fmchirp] timeoutSec: 20 expect: exitCode: 0 stdoutContains: - "LeastSquares and FMChirp smoke tests passed"

object LeastSquaresAndFMChirpSmokeTest {

fun runTests(): Boolean

    fun runTests(): Boolean {
        return try {
            println("Running LeastSquares and FMChirp smoke tests...")
            testLeastSquaresFindsSmallNonlinearSolution()
            testFMChirpPointModifier()
            testFMChirpOrderedAreaModifier()
            println("LeastSquares and FMChirp smoke tests passed")
            true
        } catch (t: Throwable) {
            println("LeastSquares and FMChirp smoke tests failed: ${t.message}")
            t.printStackTrace()
            false
        }
    }

fun main(args: Array<String>)

body — 63 lines, from line 38
    @JvmStatic
    fun main(args: Array<String>) {
        check(runTests()) { "LeastSquares and FMChirp smoke tests failed" }
    }

    private fun testLeastSquaresFindsSmallNonlinearSolution() {
        val result = leastSquares(
            initial = doubleArrayOf(0.2, -0.1),
            options = LeastSquaresOptions(maxIterations = 80)
        ) { x ->
            doubleArrayOf(
                x[0] + 2.0 * x[1] - 1.0,
                sin(x[0]) - 0.25
            )
        }

        require(result.cost < 1e-10) { "Expected tiny least-squares cost, got ${result.cost}" }
        require(abs(result.x[0] + 2.0 * result.x[1] - 1.0) < 1e-5) { "Linear residual remained high" }
        require(abs(sin(result.x[0]) - 0.25) < 1e-5) { "Nonlinear residual remained high" }
    }

    private fun testFMChirpPointModifier() {
        val base = FMChirpCurves.Presets.neutral()
        val targetX = 0.5f
        val targetY = 0.68f
        val shaped = base.shape(
            FMChirpCurves.Modifiers.hit(targetX, targetY, weight = 25f),
            options = FMChirpCurves.FitOptions(
                leastSquares = LeastSquaresOptions(maxIterations = 60),
                regularizationWeight = 0.01f
            )
        )

        val beforeError = abs(base.eval(targetX) - targetY)
        val afterError = abs(shaped.eval(targetX) - targetY)
        require(afterError < beforeError * 0.35f) {
            "Point modifier did not improve enough: before=$beforeError after=$afterError shaped=$shaped"
        }
    }

    private fun testFMChirpOrderedAreaModifier() {
        val base = FMChirpCurves.Presets.neutral()
        val targetX = 0.5f
        val targetY = 0.62f
        val shaped = base.shape(
            FMChirpCurves.Modifiers.hit(targetX, targetY, weight = 18f),
            FMChirpCurves.Modifiers.areaDelta(0.08f, weight = 8f),
            options = FMChirpCurves.FitOptions(
                leastSquares = LeastSquaresOptions(maxIterations = 60),
                regularizationWeight = 0.02f
            )
        )

        val baseArea = base.area()
        val shapedArea = shaped.area()
        require(shapedArea > baseArea + 0.025f) {
            "Area modifier did not increase area enough: base=$baseArea shaped=$shapedArea"
        }
        require(abs(shaped.eval(targetX) - targetY) < 0.18f) {
            "Ordered modifiers wandered too far from hit target: y=${shaped.eval(targetX)} target=$targetY"
        }
    }
}