From 8ab423547eb4ecc8188bc5bac40e1d0b8a751669 Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Fri, 30 May 2025 23:19:44 +0200 Subject: [PATCH 01/19] Add sample jvm cli for testing MNIST --- .../commonTest/kotlin/sk/ai/net/ShapeTest.kt | 3 +- gradle/libs.versions.toml | 4 +- gradle/wrapper/gradle-wrapper.properties | 2 +- samples/mnist-mlp-cli/build.gradle.kts | 69 +++++++++++++++ .../sk/ai/net/samples/sinus/mlp/Main.kt | 10 +++ .../sk/ai/net/samples/mnist/mlp/Main.kt | 87 +++++++++++++++++++ settings.gradle.kts | 1 + 7 files changed, 173 insertions(+), 3 deletions(-) create mode 100644 samples/mnist-mlp-cli/build.gradle.kts create mode 100644 samples/mnist-mlp-cli/src/commonMain/kotlin/sk/ai/net/samples/sinus/mlp/Main.kt create mode 100644 samples/mnist-mlp-cli/src/jvmMain/kotlin/sk/ai/net/samples/mnist/mlp/Main.kt diff --git a/core/src/commonTest/kotlin/sk/ai/net/ShapeTest.kt b/core/src/commonTest/kotlin/sk/ai/net/ShapeTest.kt index 0df51145..9ac5f7fa 100644 --- a/core/src/commonTest/kotlin/sk/ai/net/ShapeTest.kt +++ b/core/src/commonTest/kotlin/sk/ai/net/ShapeTest.kt @@ -2,12 +2,13 @@ package sk.ai.net import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertNotEquals class ShapeTest { @Test fun `test scalar`() { val shape = Shape(0) - assertEquals(shape, Shape(1, 2)) + assertNotEquals(shape, Shape(1, 2)) } } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f4dbdc49..73ce053b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.10.1" +agp = "8.10.0-alpha05" kotlin = "2.1.21" kotlinx-coroutines = "1.10.2" android-minSdk = "24" @@ -10,6 +10,7 @@ testng = "7.10.2" binaryCompatibility = "0.17.0" moduleGraphSouza = "0.12.0" kotlinxIo = "0.7.0" +kotlinxCli = "0.3.5" [libraries] kotlinx-coroutines = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } @@ -17,6 +18,7 @@ kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotl kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } nexus-publish = { module = "io.github.gradle-nexus.publish-plugin:io.github.gradle-nexus.publish-plugin.gradle.plugin", version.ref = "nexus-publish" } kotlinx-io-core = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version.ref = "kotlinxIo" } +kotlinx-cli = { module = "org.jetbrains.kotlinx:kotlinx-cli", version.ref = "kotlinxCli" } [plugins] androidLibrary = { id = "com.android.library", version.ref = "agp" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 5c40527d..4eaec467 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/samples/mnist-mlp-cli/build.gradle.kts b/samples/mnist-mlp-cli/build.gradle.kts new file mode 100644 index 00000000..8b506174 --- /dev/null +++ b/samples/mnist-mlp-cli/build.gradle.kts @@ -0,0 +1,69 @@ +plugins { + kotlin("multiplatform") +} + +// Disable default hierarchy template to avoid warnings +kotlin.sourceSets.all { + languageSettings.optIn("kotlin.RequiresOptIn") +} + +kotlin { + // Configure JVM toolchain at the extension level + jvmToolchain(11) + + jvm { + // JVM target configuration + compilations.all { + compileTaskProvider.configure { + compilerOptions.jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11) + } + } + } + + // Only include JVM target for now to simplify the build + // We'll add native targets back once we resolve the dependency issues + + sourceSets { + val commonMain by getting { + dependencies { + implementation(project(":io")) + implementation(project(":gguf")) + implementation(project(":core")) + implementation(libs.kotlinx.io.core) + implementation(libs.kotlinx.coroutines) + + implementation(libs.kotlinx.cli) + } + } + + val jvmMain by getting { + dependencies { + } + } + } +} + +// Configure JVM jar task +tasks.register("jvmFatJar") { + dependsOn(tasks.named("jvmJar")) + archiveClassifier.set("fat") + + manifest { + attributes( + "Main-Class" to "sk.ai.net.samples.sinus.mlp.MainKt" + ) + } + + from( + configurations.named("jvmRuntimeClasspath").get().map { + if (it.isDirectory) it else zipTree(it) + } + ) + + with(tasks.named("jvmJar").get()) + + // Exclude META-INF files from the dependencies + exclude("META-INF/*.SF", "META-INF/*.DSA", "META-INF/*.RSA") + + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} diff --git a/samples/mnist-mlp-cli/src/commonMain/kotlin/sk/ai/net/samples/sinus/mlp/Main.kt b/samples/mnist-mlp-cli/src/commonMain/kotlin/sk/ai/net/samples/sinus/mlp/Main.kt new file mode 100644 index 00000000..acb96000 --- /dev/null +++ b/samples/mnist-mlp-cli/src/commonMain/kotlin/sk/ai/net/samples/sinus/mlp/Main.kt @@ -0,0 +1,10 @@ +package sk.ai.net.samples.mnist.mlp + +/** + * Main entry point for the sinus MLP CLI application. + * + * This application loads weights from a GGUF or SafeTensors file and + * approximates the value of sine using an MLP network with one input (angle), + * 2 hidden layers, and 1 output neuron. + */ +expect fun main(args: Array) \ No newline at end of file diff --git a/samples/mnist-mlp-cli/src/jvmMain/kotlin/sk/ai/net/samples/mnist/mlp/Main.kt b/samples/mnist-mlp-cli/src/jvmMain/kotlin/sk/ai/net/samples/mnist/mlp/Main.kt new file mode 100644 index 00000000..b0211d66 --- /dev/null +++ b/samples/mnist-mlp-cli/src/jvmMain/kotlin/sk/ai/net/samples/mnist/mlp/Main.kt @@ -0,0 +1,87 @@ +package sk.ai.net.samples.mnist.mlp + +import kotlinx.cli.ArgParser +import kotlinx.cli.ArgType +import kotlinx.cli.required +import sk.ai.net.Shape +import sk.ai.net.Tensor +import sk.ai.net.dsl.network +import sk.ai.net.impl.DoublesTensor +import sk.ai.net.nn.activations.ReLU +import java.io.File + +/** + * Main entry point for the sinus-mlp-cli application. + * + * This application loads weights from a GGUF or SafeTensors file and uses an MLP network + * to approximate the sine function. + */ +actual fun main(args: Array) { + val parser = ArgParser("sinus-mlp-cli") + val modelPath by parser.option( + ArgType.String, + shortName = "m", + fullName = "model", + description = "Path to the model file (GGUF or SafeTensors)" + ).required() + + parser.parse(args) + + println("MNIST MLP CLI") + println("=============") + println("Model file: $modelPath") + println() + + try { + // Check if the model file exists + val modelFile = File(modelPath) + if (!modelFile.exists()) { + println("Error: Model file not found: $modelPath") + return + } + + // In a real implementation, we would use ModelFormatLoader to load the model + // But for this simplified example, we'll create a simple MLP directly + println("Creating MLP network...") + val mlp = createSimpleMLP() + + + } catch (e: Exception) { + println("Error: ${e.message}") + e.printStackTrace() + } +} + +/** + * Creates a simple MLP network for sine approximation. + * + * The network has one input (angle), two hidden layers with 10 neurons each, and one output neuron. + * + * @return The MLP network. + */ +private fun createSimpleMLP(): sk.ai.net.nn.Module { + // Create a context for the DSL + // Create the MLP network using the DSL + return network { + input(1) // One input neuron for the angle + dense(10) { // First hidden layer with 10 neurons + activation = ReLU()::invoke + } + dense(10) { // Second hidden layer with 10 neurons + activation = ReLU()::invoke + } + dense(1) { // Output layer with 1 neuron + // No activation for the output layer (linear) + } + } +} + +/** + * Creates an input tensor for the given angle. + * + * @param angle The angle in radians. + * @return A tensor representing the angle. + */ +private fun createInputTensor(angle: Double): Tensor { + return DoublesTensor (Shape(1), doubleArrayOf(angle)) +} diff --git a/settings.gradle.kts b/settings.gradle.kts index b2d99c80..9ff9ff21 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -17,3 +17,4 @@ rootProject.name = "skainet" include(":core") include(":io") include(":gguf") +include(":samples:mnist-mlp-cli") From 4ad07a6e5902e44bfae19684adcaf8b55829d4da Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Sat, 31 May 2025 13:43:24 +0200 Subject: [PATCH 02/19] Implement MNIST data loading as KMP module. Based on Kotlin Notebook https://github.com/sk-ai-net/skainet/blob/feature/MNIST/samples/notebooks/MNIST-dataloader.ipynb Related-To: #3 --- kotlin-js-store/yarn.lock | 2070 +++++++++++++++++ model-zoo/build.gradle.kts | 95 + .../net/io/data/mnist/MNISTLoaderAndroid.kt | 131 ++ .../net/io/data/mnist/MNISTLoaderFactory.kt | 35 + .../sk/ai/net/io/data/mnist/MNISTData.kt | 108 + .../ai/net/io/data/mnist/MNISTLoaderCommon.kt | 126 + .../net/io/data/mnist/MNISTLoaderFactory.kt | 29 + .../sk/ai/net/io/data/mnist/MNISTDataTest.kt | 106 + .../net/io/data/mnist/MNISTLoaderFactory.kt | 35 + .../sk/ai/net/io/data/mnist/MNISTLoaderIos.kt | 87 + .../sk/ai/net/io/data/mnist/MNISTLoader.kt | 112 + .../net/io/data/mnist/MNISTLoaderFactory.kt | 35 + .../sk/ai/net/io/data/mnist/MNISTLoaderJvm.kt | 140 ++ .../ai/net/io/data/mnist/MNISTLoaderTest.kt | 130 ++ .../net/io/data/mnist/MNISTLoaderFactory.kt | 35 + .../ai/net/io/data/mnist/MNISTLoaderWasmJs.kt | 87 + settings.gradle.kts | 1 + 17 files changed, 3362 insertions(+) create mode 100644 kotlin-js-store/yarn.lock create mode 100644 model-zoo/build.gradle.kts create mode 100644 model-zoo/src/androidMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderAndroid.kt create mode 100644 model-zoo/src/androidMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderFactory.kt create mode 100644 model-zoo/src/commonMain/kotlin/sk/ai/net/io/data/mnist/MNISTData.kt create mode 100644 model-zoo/src/commonMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderCommon.kt create mode 100644 model-zoo/src/commonMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderFactory.kt create mode 100644 model-zoo/src/commonTest/kotlin/sk/ai/net/io/data/mnist/MNISTDataTest.kt create mode 100644 model-zoo/src/iosMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderFactory.kt create mode 100644 model-zoo/src/iosMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderIos.kt create mode 100644 model-zoo/src/jvmMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoader.kt create mode 100644 model-zoo/src/jvmMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderFactory.kt create mode 100644 model-zoo/src/jvmMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderJvm.kt create mode 100644 model-zoo/src/jvmTest/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderTest.kt create mode 100644 model-zoo/src/wasmJsMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderFactory.kt create mode 100644 model-zoo/src/wasmJsMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderWasmJs.kt diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock new file mode 100644 index 00000000..fd1f1e87 --- /dev/null +++ b/kotlin-js-store/yarn.lock @@ -0,0 +1,2070 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@colors/colors@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" + integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== + +"@discoveryjs/json-ext@^0.5.0": + version "0.5.7" + resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" + integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== + +"@jridgewell/gen-mapping@^0.3.5": + version "0.3.8" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz#4f0e06362e01362f823d348f1872b08f666d8142" + integrity sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA== + dependencies: + "@jridgewell/set-array" "^1.2.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + +"@jridgewell/source-map@^0.3.3": + version "0.3.6" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.6.tgz#9d71ca886e32502eb9362c9a74a46787c36df81a" + integrity sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@socket.io/component-emitter@~3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz#821f8442f4175d8f0467b9daf26e3a18e2d02af2" + integrity sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA== + +"@types/cors@^2.8.12": + version "2.8.18" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.18.tgz#101e033b3ca06695f3d73c587cd7f9eb348135d1" + integrity sha512-nX3d0sxJW41CqQvfOzVG1NCTXfFDrDWIghCZncpHeWlVFd81zxB/DLhg7avFg6eHLCRX7ckBmoIIcqa++upvJA== + dependencies: + "@types/node" "*" + +"@types/estree@^1.0.5": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8" + integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ== + +"@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +"@types/node@*", "@types/node@>=10.0.0": + version "22.15.29" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.15.29.tgz#c75999124a8224a3f79dd8b6ccfb37d74098f678" + integrity sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ== + dependencies: + undici-types "~6.21.0" + +"@webassemblyjs/ast@1.14.1", "@webassemblyjs/ast@^1.12.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.14.1.tgz#a9f6a07f2b03c95c8d38c4536a1fdfb521ff55b6" + integrity sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ== + dependencies: + "@webassemblyjs/helper-numbers" "1.13.2" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + +"@webassemblyjs/floating-point-hex-parser@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz#fcca1eeddb1cc4e7b6eed4fc7956d6813b21b9fb" + integrity sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA== + +"@webassemblyjs/helper-api-error@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz#e0a16152248bc38daee76dd7e21f15c5ef3ab1e7" + integrity sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ== + +"@webassemblyjs/helper-buffer@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz#822a9bc603166531f7d5df84e67b5bf99b72b96b" + integrity sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA== + +"@webassemblyjs/helper-numbers@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz#dbd932548e7119f4b8a7877fd5a8d20e63490b2d" + integrity sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA== + dependencies: + "@webassemblyjs/floating-point-hex-parser" "1.13.2" + "@webassemblyjs/helper-api-error" "1.13.2" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/helper-wasm-bytecode@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz#e556108758f448aae84c850e593ce18a0eb31e0b" + integrity sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA== + +"@webassemblyjs/helper-wasm-section@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz#9629dda9c4430eab54b591053d6dc6f3ba050348" + integrity sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-buffer" "1.14.1" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/wasm-gen" "1.14.1" + +"@webassemblyjs/ieee754@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz#1c5eaace1d606ada2c7fd7045ea9356c59ee0dba" + integrity sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.13.2.tgz#57c5c3deb0105d02ce25fa3fd74f4ebc9fd0bbb0" + integrity sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.13.2.tgz#917a20e93f71ad5602966c2d685ae0c6c21f60f1" + integrity sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ== + +"@webassemblyjs/wasm-edit@^1.12.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz#ac6689f502219b59198ddec42dcd496b1004d597" + integrity sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-buffer" "1.14.1" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/helper-wasm-section" "1.14.1" + "@webassemblyjs/wasm-gen" "1.14.1" + "@webassemblyjs/wasm-opt" "1.14.1" + "@webassemblyjs/wasm-parser" "1.14.1" + "@webassemblyjs/wast-printer" "1.14.1" + +"@webassemblyjs/wasm-gen@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz#991e7f0c090cb0bb62bbac882076e3d219da9570" + integrity sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/ieee754" "1.13.2" + "@webassemblyjs/leb128" "1.13.2" + "@webassemblyjs/utf8" "1.13.2" + +"@webassemblyjs/wasm-opt@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz#e6f71ed7ccae46781c206017d3c14c50efa8106b" + integrity sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-buffer" "1.14.1" + "@webassemblyjs/wasm-gen" "1.14.1" + "@webassemblyjs/wasm-parser" "1.14.1" + +"@webassemblyjs/wasm-parser@1.14.1", "@webassemblyjs/wasm-parser@^1.12.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz#b3e13f1893605ca78b52c68e54cf6a865f90b9fb" + integrity sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-api-error" "1.13.2" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/ieee754" "1.13.2" + "@webassemblyjs/leb128" "1.13.2" + "@webassemblyjs/utf8" "1.13.2" + +"@webassemblyjs/wast-printer@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz#3bb3e9638a8ae5fdaf9610e7a06b4d9f9aa6fe07" + integrity sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@xtuc/long" "4.2.2" + +"@webpack-cli/configtest@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-2.1.1.tgz#3b2f852e91dac6e3b85fb2a314fb8bef46d94646" + integrity sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw== + +"@webpack-cli/info@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-2.0.2.tgz#cc3fbf22efeb88ff62310cf885c5b09f44ae0fdd" + integrity sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A== + +"@webpack-cli/serve@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.5.tgz#325db42395cd49fe6c14057f9a900e427df8810e" + integrity sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ== + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +accepts@~1.3.4: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +acorn-import-attributes@^1.9.5: + version "1.9.5" + resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef" + integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ== + +acorn@^8.14.0, acorn@^8.7.1: + version "8.14.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.1.tgz#721d5dc10f7d5b5609a891773d47731796935dfb" + integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg== + +ajv-formats@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" + integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== + dependencies: + ajv "^8.0.0" + +ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv-keywords@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" + integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== + dependencies: + fast-deep-equal "^3.1.3" + +ajv@^6.12.5: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ajv@^8.0.0, ajv@^8.9.0: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" + integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== + dependencies: + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + +ansi-colors@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" + integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base64id@2.0.0, base64id@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" + integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog== + +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + +body-parser@^1.19.0: + version "1.20.3" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" + integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.13.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +braces@^3.0.2, braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +browser-stdout@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== + +browserslist@^4.21.10: + version "4.25.0" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.25.0.tgz#986aa9c6d87916885da2b50d8eb577ac8d133b2c" + integrity sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA== + dependencies: + caniuse-lite "^1.0.30001718" + electron-to-chromium "^1.5.160" + node-releases "^2.0.19" + update-browserslist-db "^1.1.3" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + +call-bound@^1.0.2, call-bound@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + dependencies: + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" + +camelcase@^6.0.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +caniuse-lite@^1.0.30001718: + version "1.0.30001720" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001720.tgz#c138cb6026d362be9d8d7b0e4bcd0183a850edfd" + integrity sha512-Ec/2yV2nNPwb4DnTANEV99ZWwm3ZWfdlfkQbWSDDt+PsXEVYwlhPH8tdMaPunYTKKmz7AnHi2oNEi1GcmKCD8g== + +chalk@^4.1.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chokidar@^3.5.1, chokidar@^3.5.3: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chrome-trace-event@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz#05bffd7ff928465093314708c93bdfa9bd1f0f5b" + integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ== + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +clone-deep@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" + integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== + dependencies: + is-plain-object "^2.0.4" + kind-of "^6.0.2" + shallow-clone "^3.0.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +colorette@^2.0.14: + version "2.0.20" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== + +commander@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" + integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +connect@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/connect/-/connect-3.7.0.tgz#5d49348910caa5e07a01800b030d0c35f20484f8" + integrity sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ== + dependencies: + debug "2.6.9" + finalhandler "1.1.2" + parseurl "~1.3.3" + utils-merge "1.0.1" + +content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +cookie@~0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" + integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== + +cors@~2.8.5: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + +cross-spawn@^7.0.3: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +custom-event@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425" + integrity sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg== + +date-format@^4.0.14: + version "4.0.14" + resolved "https://registry.yarnpkg.com/date-format/-/date-format-4.0.14.tgz#7a8e584434fb169a521c8b7aa481f355810d9400" + integrity sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg== + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^4.3.4, debug@^4.3.5: + version "4.4.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b" + integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ== + dependencies: + ms "^2.1.3" + +debug@~4.3.1, debug@~4.3.2, debug@~4.3.4: + version "4.3.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== + dependencies: + ms "^2.1.3" + +decamelize@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" + integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +di@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c" + integrity sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA== + +diff@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" + integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A== + +dom-serialize@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b" + integrity sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ== + dependencies: + custom-event "~1.0.0" + ent "~2.2.0" + extend "^3.0.0" + void-elements "^2.0.0" + +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +electron-to-chromium@^1.5.160: + version "1.5.161" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.161.tgz#650376bd3be7ff8e581031409fc2d4f150620b12" + integrity sha512-hwtetwfKNZo/UlwHIVBlKZVdy7o8bIZxxKs0Mv/ROPiQQQmDgdm5a+KvKtBsxM8ZjFzTaCeLoodZ8jiBE3o9rA== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +engine.io-parser@~5.2.1: + version "5.2.3" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.3.tgz#00dc5b97b1f233a23c9398d0209504cf5f94d92f" + integrity sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q== + +engine.io@~6.6.0: + version "6.6.4" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.6.4.tgz#0a89a3e6b6c1d4b0c2a2a637495e7c149ec8d8ee" + integrity sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g== + dependencies: + "@types/cors" "^2.8.12" + "@types/node" ">=10.0.0" + accepts "~1.3.4" + base64id "2.0.0" + cookie "~0.7.2" + cors "~2.8.5" + debug "~4.3.1" + engine.io-parser "~5.2.1" + ws "~8.17.1" + +enhanced-resolve@^5.17.1: + version "5.18.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz#728ab082f8b7b6836de51f1637aab5d3b9568faf" + integrity sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +ent@~2.2.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.2.tgz#22a5ed2fd7ce0cbcff1d1474cf4909a44bdb6e85" + integrity sha512-kKvD1tO6BM+oK9HzCPpUdRb4vKFQY/FPTFmurMvh6LlN68VMrdj77w8yp51/kDbpkFOS9J8w5W6zIzgM2H8/hw== + dependencies: + call-bound "^1.0.3" + es-errors "^1.3.0" + punycode "^1.4.1" + safe-regex-test "^1.1.0" + +envinfo@^7.7.3: + version "7.14.0" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.14.0.tgz#26dac5db54418f2a4c1159153a0b2ae980838aae" + integrity sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg== + +es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-module-lexer@^1.2.1: + version "1.7.0" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz#9159601561880a85f2734560a9099b2c31e5372a" + integrity sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA== + +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== + dependencies: + es-errors "^1.3.0" + +escalade@^3.1.1, escalade@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-scope@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +eventemitter3@^4.0.0: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + +events@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +extend@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-uri@^3.0.1: + version "3.0.6" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.6.tgz#88f130b77cfaea2378d56bf970dea21257a68748" + integrity sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw== + +fastest-levenshtein@^1.0.12: + version "1.0.16" + resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" + integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + +find-up@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + +flatted@^3.2.7: + version "3.3.3" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358" + integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== + +follow-redirects@^1.0.0: + version "1.15.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" + integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== + +format-util@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/format-util/-/format-util-1.0.5.tgz#1ffb450c8a03e7bccffe40643180918cc297d271" + integrity sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg== + +fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-intrinsic@^1.2.5, get-intrinsic@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== + dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + function-bind "^1.1.2" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + +get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +glob@^7.1.3, glob@^7.1.7: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" + integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^5.0.1" + once "^1.3.0" + +gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.10, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-symbols@^1.0.3, has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + +has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +he@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-proxy@^1.18.1: + version "1.18.1" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" + integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== + dependencies: + eventemitter3 "^4.0.0" + follow-redirects "^1.0.0" + requires-port "^1.0.0" + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +iconv-lite@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +import-local@^3.0.2: + version "3.2.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.2.0.tgz#c3d5c745798c02a6f8b897726aba5100186ee260" + integrity sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +interpret@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-3.1.1.tgz#5be0ceed67ca79c6c4bc5cf0d7ee843dcea110c4" + integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-core-module@^2.16.0: + version "2.16.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" + integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== + dependencies: + hasown "^2.0.2" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-plain-obj@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + +is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-regex@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.2.1.tgz#76d70a3ed10ef9be48eb577887d74205bf0cad22" + integrity sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g== + dependencies: + call-bound "^1.0.2" + gopd "^1.2.0" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + +isbinaryfile@^4.0.8: + version "4.0.10" + resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.10.tgz#0c5b5e30c2557a2f06febd37b7322946aaee42b3" + integrity sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== + +jest-worker@^27.4.5: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" + integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +json-parse-even-better-errors@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg== + optionalDependencies: + graceful-fs "^4.1.6" + +karma-chrome-launcher@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz#eb9c95024f2d6dfbb3748d3415ac9b381906b9a9" + integrity sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q== + dependencies: + which "^1.2.1" + +karma-mocha@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/karma-mocha/-/karma-mocha-2.0.1.tgz#4b0254a18dfee71bdbe6188d9a6861bf86b0cd7d" + integrity sha512-Tzd5HBjm8his2OA4bouAsATYEpZrp9vC7z5E5j4C5Of5Rrs1jY67RAwXNcVmd/Bnk1wgvQRou0zGVLey44G4tQ== + dependencies: + minimist "^1.2.3" + +karma-sourcemap-loader@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/karma-sourcemap-loader/-/karma-sourcemap-loader-0.4.0.tgz#b01d73f8f688f533bcc8f5d273d43458e13b5488" + integrity sha512-xCRL3/pmhAYF3I6qOrcn0uhbQevitc2DERMPH82FMnG+4WReoGcGFZb1pURf2a5apyrOHRdvD+O6K7NljqKHyA== + dependencies: + graceful-fs "^4.2.10" + +karma-webpack@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/karma-webpack/-/karma-webpack-5.0.1.tgz#4eafd31bbe684a747a6e8f3e4ad373e53979ced4" + integrity sha512-oo38O+P3W2mSPCSUrQdySSPv1LvPpXP+f+bBimNomS5sW+1V4SuhCuW8TfJzV+rDv921w2fDSDw0xJbPe6U+kQ== + dependencies: + glob "^7.1.3" + minimatch "^9.0.3" + webpack-merge "^4.1.5" + +karma@6.4.4: + version "6.4.4" + resolved "https://registry.yarnpkg.com/karma/-/karma-6.4.4.tgz#dfa5a426cf5a8b53b43cd54ef0d0d09742351492" + integrity sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w== + dependencies: + "@colors/colors" "1.5.0" + body-parser "^1.19.0" + braces "^3.0.2" + chokidar "^3.5.1" + connect "^3.7.0" + di "^0.0.1" + dom-serialize "^2.2.1" + glob "^7.1.7" + graceful-fs "^4.2.6" + http-proxy "^1.18.1" + isbinaryfile "^4.0.8" + lodash "^4.17.21" + log4js "^6.4.1" + mime "^2.5.2" + minimatch "^3.0.4" + mkdirp "^0.5.5" + qjobs "^1.2.0" + range-parser "^1.2.1" + rimraf "^3.0.2" + socket.io "^4.7.2" + source-map "^0.6.1" + tmp "^0.2.1" + ua-parser-js "^0.7.30" + yargs "^16.1.1" + +kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +kotlin-web-helpers@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/kotlin-web-helpers/-/kotlin-web-helpers-2.0.0.tgz#b112096b273c1e733e0b86560998235c09a19286" + integrity sha512-xkVGl60Ygn/zuLkDPx+oHj7jeLR7hCvoNF99nhwXMn8a3ApB4lLiC9pk4ol4NHPjyoCbvQctBqvzUcp8pkqyWw== + dependencies: + format-util "^1.0.5" + +loader-runner@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" + integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash@^4.17.15, lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +log-symbols@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +log4js@^6.4.1: + version "6.9.1" + resolved "https://registry.yarnpkg.com/log4js/-/log4js-6.9.1.tgz#aba5a3ff4e7872ae34f8b4c533706753709e38b6" + integrity sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g== + dependencies: + date-format "^4.0.14" + debug "^4.3.4" + flatted "^3.2.7" + rfdc "^1.3.0" + streamroller "^3.1.5" + +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.27, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@^2.5.2: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== + +minimatch@^3.0.4, minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^5.0.1, minimatch@^5.1.6: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^9.0.3: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + +minimist@^1.2.3, minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +mkdirp@^0.5.5: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +mocha@10.7.3: + version "10.7.3" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.7.3.tgz#ae32003cabbd52b59aece17846056a68eb4b0752" + integrity sha512-uQWxAu44wwiACGqjbPYmjo7Lg8sFrS3dQe7PP2FQI+woptP4vZXSMcfMyFL/e1yFEeEpV4RtyTpZROOKmxis+A== + dependencies: + ansi-colors "^4.1.3" + browser-stdout "^1.3.1" + chokidar "^3.5.3" + debug "^4.3.5" + diff "^5.2.0" + escape-string-regexp "^4.0.0" + find-up "^5.0.0" + glob "^8.1.0" + he "^1.2.0" + js-yaml "^4.1.0" + log-symbols "^4.1.0" + minimatch "^5.1.6" + ms "^2.1.3" + serialize-javascript "^6.0.2" + strip-json-comments "^3.1.1" + supports-color "^8.1.1" + workerpool "^6.5.1" + yargs "^16.2.0" + yargs-parser "^20.2.9" + yargs-unparser "^2.0.0" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +node-releases@^2.0.19: + version "2.0.19" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" + integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +object-assign@^4: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-inspect@^1.13.3: + version "1.13.4" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" + integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww== + dependencies: + ee-first "1.1.1" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +picomatch@^2.0.4, picomatch@^2.2.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== + +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +qjobs@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071" + integrity sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg== + +qs@6.13.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" + integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== + dependencies: + side-channel "^1.0.6" + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +range-parser@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +rechoir@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.8.0.tgz#49f866e0d32146142da3ad8f0eff352b3215ff22" + integrity sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ== + dependencies: + resolve "^1.20.0" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve@^1.20.0: + version "1.22.10" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.10.tgz#b663e83ffb09bbf2386944736baae803029b8b39" + integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w== + dependencies: + is-core-module "^2.16.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +rfdc@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca" + integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +safe-buffer@^5.1.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-regex-test@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz#7f87dfb67a3150782eaaf18583ff5d1711ac10c1" + integrity sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + is-regex "^1.2.1" + +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +schema-utils@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" + integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + +schema-utils@^4.3.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.3.2.tgz#0c10878bf4a73fd2b1dfd14b9462b26788c806ae" + integrity sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ== + dependencies: + "@types/json-schema" "^7.0.9" + ajv "^8.9.0" + ajv-formats "^2.1.1" + ajv-keywords "^5.1.0" + +serialize-javascript@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" + integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== + dependencies: + randombytes "^2.1.0" + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== + dependencies: + kind-of "^6.0.2" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +side-channel-list@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" + integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + +side-channel@^1.0.6: + version "1.1.0" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + side-channel-list "^1.0.0" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" + +socket.io-adapter@~2.5.2: + version "2.5.5" + resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz#c7a1f9c703d7756844751b6ff9abfc1780664082" + integrity sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg== + dependencies: + debug "~4.3.4" + ws "~8.17.1" + +socket.io-parser@~4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83" + integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + +socket.io@^4.7.2: + version "4.8.1" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.8.1.tgz#fa0eaff965cc97fdf4245e8d4794618459f7558a" + integrity sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg== + dependencies: + accepts "~1.3.4" + base64id "~2.0.0" + cors "~2.8.5" + debug "~4.3.2" + engine.io "~6.6.0" + socket.io-adapter "~2.5.2" + socket.io-parser "~4.2.4" + +source-map-js@^1.0.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + +source-map-loader@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-5.0.0.tgz#f593a916e1cc54471cfc8851b905c8a845fc7e38" + integrity sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA== + dependencies: + iconv-lite "^0.6.3" + source-map-js "^1.0.2" + +source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0, source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +statuses@~1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== + +streamroller@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-3.1.5.tgz#1263182329a45def1ffaef58d31b15d13d2ee7ff" + integrity sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw== + dependencies: + date-format "^4.0.14" + debug "^4.3.4" + fs-extra "^8.1.0" + +string-width@^4.1.0, string-width@^4.2.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.0.0, supports-color@^8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +tapable@^2.1.1, tapable@^2.2.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.2.tgz#ab4984340d30cb9989a490032f086dbb8b56d872" + integrity sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg== + +terser-webpack-plugin@^5.3.10: + version "5.3.14" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz#9031d48e57ab27567f02ace85c7d690db66c3e06" + integrity sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw== + dependencies: + "@jridgewell/trace-mapping" "^0.3.25" + jest-worker "^27.4.5" + schema-utils "^4.3.0" + serialize-javascript "^6.0.2" + terser "^5.31.1" + +terser@^5.31.1: + version "5.40.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.40.0.tgz#839a80db42bfee8340085f44ea99b5cba36c55c8" + integrity sha512-cfeKl/jjwSR5ar7d0FGmave9hFGJT8obyo0z+CrQOylLDbk7X81nPU6vq9VORa5jU30SkDnT2FXjLbR8HLP+xA== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.14.0" + commander "^2.20.0" + source-map-support "~0.5.20" + +tmp@^0.2.1: + version "0.2.3" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.3.tgz#eb783cc22bc1e8bebd0671476d46ea4eb32a79ae" + integrity sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typescript@5.5.4: + version "5.5.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba" + integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q== + +ua-parser-js@^0.7.30: + version "0.7.40" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.40.tgz#c87d83b7bb25822ecfa6397a0da5903934ea1562" + integrity sha512-us1E3K+3jJppDBa3Tl0L3MOJiGhe1C6P0+nIvQAFYbxlMAx0h81eOwLmU57xgqToduDDPx3y5QsdjPfDu+FgOQ== + +undici-types@~6.21.0: + version "6.21.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb" + integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ== + +universalify@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +update-browserslist-db@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420" + integrity sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.1" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +vary@^1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +void-elements@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" + integrity sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung== + +watchpack@^2.4.1: + version "2.4.4" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.4.tgz#473bda72f0850453da6425081ea46fc0d7602947" + integrity sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" + +webpack-cli@5.1.4: + version "5.1.4" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-5.1.4.tgz#c8e046ba7eaae4911d7e71e2b25b776fcc35759b" + integrity sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg== + dependencies: + "@discoveryjs/json-ext" "^0.5.0" + "@webpack-cli/configtest" "^2.1.1" + "@webpack-cli/info" "^2.0.2" + "@webpack-cli/serve" "^2.0.5" + colorette "^2.0.14" + commander "^10.0.1" + cross-spawn "^7.0.3" + envinfo "^7.7.3" + fastest-levenshtein "^1.0.12" + import-local "^3.0.2" + interpret "^3.1.1" + rechoir "^0.8.0" + webpack-merge "^5.7.3" + +webpack-merge@^4.1.5: + version "4.2.2" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.2.2.tgz#a27c52ea783d1398afd2087f547d7b9d2f43634d" + integrity sha512-TUE1UGoTX2Cd42j3krGYqObZbOD+xF7u28WB7tfUordytSjbWTIjK/8V0amkBfTYN4/pB/GIDlJZZ657BGG19g== + dependencies: + lodash "^4.17.15" + +webpack-merge@^5.7.3: + version "5.10.0" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.10.0.tgz#a3ad5d773241e9c682803abf628d4cd62b8a4177" + integrity sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA== + dependencies: + clone-deep "^4.0.1" + flat "^5.0.2" + wildcard "^2.0.0" + +webpack-sources@^3.2.3: + version "3.3.0" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.3.0.tgz#8d3449f1ed3f254e722a529a0a344a37d2d17048" + integrity sha512-77R0RDmJfj9dyv5p3bM5pOHa+X8/ZkO9c7kpDstigkC4nIDobadsfSGCwB4bKhMVxqAok8tajaoR8rirM7+VFQ== + +webpack@5.94.0: + version "5.94.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.94.0.tgz#77a6089c716e7ab90c1c67574a28da518a20970f" + integrity sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg== + dependencies: + "@types/estree" "^1.0.5" + "@webassemblyjs/ast" "^1.12.1" + "@webassemblyjs/wasm-edit" "^1.12.1" + "@webassemblyjs/wasm-parser" "^1.12.1" + acorn "^8.7.1" + acorn-import-attributes "^1.9.5" + browserslist "^4.21.10" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.17.1" + es-module-lexer "^1.2.1" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.11" + json-parse-even-better-errors "^2.3.1" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^3.2.0" + tapable "^2.1.1" + terser-webpack-plugin "^5.3.10" + watchpack "^2.4.1" + webpack-sources "^3.2.3" + +which@^1.2.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wildcard@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" + integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== + +workerpool@^6.5.1: + version "6.5.1" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544" + integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +ws@8.18.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== + +ws@~8.17.1: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" + integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yargs-parser@^20.2.2, yargs-parser@^20.2.9: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +yargs-unparser@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" + integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== + dependencies: + camelcase "^6.0.0" + decamelize "^4.0.0" + flat "^5.0.2" + is-plain-obj "^2.1.0" + +yargs@^16.1.1, yargs@^16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== diff --git a/model-zoo/build.gradle.kts b/model-zoo/build.gradle.kts new file mode 100644 index 00000000..d979c07d --- /dev/null +++ b/model-zoo/build.gradle.kts @@ -0,0 +1,95 @@ +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.kotlinMultiplatform) + id("com.android.library") + //alias(libs.plugins.vanniktech.mavenPublish) +} + +kotlin { + jvm() + androidTarget { + compilations.all { + kotlinOptions { + jvmTarget = "1.8" + } + } + } + + wasmJs { + browser() + } + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + val commonMain by getting { + dependencies { + implementation(project(":core")) + implementation(libs.kotlinx.io.core) + implementation(libs.kotlinx.serialization.json) + implementation("io.ktor:ktor-client-core:3.1.3") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1") + } + } + + val commonTest by getting { + dependencies { + implementation(libs.kotlin.test) + } + } + + val jvmMain by getting { + dependencies { + implementation("io.ktor:ktor-client-cio:3.1.3") + implementation("io.ktor:ktor-client-plugins:3.1.1") + implementation("io.ktor:ktor-client-logging:3.1.3") + implementation("io.ktor:ktor-client-content-negotiation:3.1.3") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.8.1") + implementation("ch.qos.logback:logback-classic:1.4.14") // For logging + } + } + + val jvmTest by getting { + dependencies { + implementation(libs.kotlinx.coroutines) + } + } + + val androidMain by getting { + dependencies { + implementation("io.ktor:ktor-client-android:3.1.3") + } + } + + val wasmJsMain by getting { + dependencies { + implementation("io.ktor:ktor-client-js:3.1.3") + } + } + + val iosX64Main by getting + val iosArm64Main by getting + val iosSimulatorArm64Main by getting + val iosMain by creating { + dependsOn(commonMain) + iosX64Main.dependsOn(this) + iosArm64Main.dependsOn(this) + iosSimulatorArm64Main.dependsOn(this) + dependencies { + implementation("io.ktor:ktor-client-darwin:3.1.3") + } + } + } +} + +android { + namespace = "sk.ai.net.model.zoo" + compileSdk = 34 + defaultConfig { + minSdk = 24 + } +} diff --git a/model-zoo/src/androidMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderAndroid.kt b/model-zoo/src/androidMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderAndroid.kt new file mode 100644 index 00000000..894ba8bc --- /dev/null +++ b/model-zoo/src/androidMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderAndroid.kt @@ -0,0 +1,131 @@ +package sk.ai.net.io.data.mnist + +import io.ktor.client.HttpClient +import io.ktor.client.engine.android.Android +import io.ktor.client.request.get +import io.ktor.client.statement.HttpResponse +import io.ktor.client.call.body +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.util.zip.GZIPInputStream + +/** + * Android implementation of the MNIST loader. + * + * @property config The configuration for the MNIST loader. + */ +class MNISTLoaderAndroid(config: MNISTLoaderConfig) : MNISTLoaderCommon(config) { + + /** + * Downloads and caches a file. + * + * @param url The URL to download from. + * @param filename The name of the file to save. + * @return The bytes of the decompressed file. + */ + override suspend fun downloadAndCacheFile(url: String, filename: String): ByteArray = withContext(Dispatchers.IO) { + val cacheDir = File(config.cacheDir) + if (!cacheDir.exists()) { + cacheDir.mkdirs() + } + + val gzipFile = File(cacheDir, filename) + val decompressedFile = File(cacheDir, filename.removeSuffix(".gz")) + + // Check if the decompressed file already exists in cache + if (config.useCache && decompressedFile.exists()) { + println("Using cached file: ${decompressedFile.path}") + return@withContext decompressedFile.readBytes() + } + + // Check if the gzip file already exists in cache + if (!gzipFile.exists() || !config.useCache) { + println("Downloading file: $url") + downloadFile(url, gzipFile.path) + } else { + println("Using cached gzip file: ${gzipFile.path}") + } + + // Decompress the gzip file + println("Decompressing file: ${gzipFile.path}") + decompressGzipFile(gzipFile.path, decompressedFile.path) + + return@withContext decompressedFile.readBytes() + } + + /** + * Downloads a file from a URL. + * + * @param url The URL to download from. + * @param outputPath The path to save the file to. + */ + private suspend fun downloadFile(url: String, outputPath: String) { + val client = HttpClient(Android) { + // No plugins needed for basic functionality + } + + try { + val file = File(outputPath) + + val httpResponse: HttpResponse = client.get(url) + val responseBody: ByteArray = httpResponse.body() + file.writeBytes(responseBody) + + println("File saved to ${file.path}") + } finally { + client.close() + } + } + + /** + * Decompresses a gzip file. + * + * @param gzipFilePath The path to the gzip file. + * @param outputFilePath The path to save the decompressed file to. + */ + private fun decompressGzipFile(gzipFilePath: String, outputFilePath: String) { + GZIPInputStream(FileInputStream(gzipFilePath)).use { gzipInputStream -> + FileOutputStream(outputFilePath).use { outputStream -> + val buffer = ByteArray(1024) + var len: Int + while (gzipInputStream.read(buffer).also { len = it } > 0) { + outputStream.write(buffer, 0, len) + } + } + } + } + + companion object { + /** + * Creates a new instance of MNISTLoaderAndroid with the default configuration. + * + * @return A new instance of MNISTLoaderAndroid. + */ + fun create(): MNISTLoaderAndroid { + return MNISTLoaderAndroid(MNISTLoaderConfig()) + } + + /** + * Creates a new instance of MNISTLoaderAndroid with a custom cache directory. + * + * @param cacheDir The directory to use for caching. + * @return A new instance of MNISTLoaderAndroid. + */ + fun create(cacheDir: String): MNISTLoaderAndroid { + return MNISTLoaderAndroid(MNISTLoaderConfig(cacheDir = cacheDir)) + } + + /** + * Creates a new instance of MNISTLoaderAndroid with a custom configuration. + * + * @param config The configuration to use. + * @return A new instance of MNISTLoaderAndroid. + */ + fun create(config: MNISTLoaderConfig): MNISTLoaderAndroid { + return MNISTLoaderAndroid(config) + } + } +} diff --git a/model-zoo/src/androidMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderFactory.kt b/model-zoo/src/androidMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderFactory.kt new file mode 100644 index 00000000..be3b555a --- /dev/null +++ b/model-zoo/src/androidMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderFactory.kt @@ -0,0 +1,35 @@ +package sk.ai.net.io.data.mnist + +/** + * Android implementation of the MNIST loader factory. + */ +actual object MNISTLoaderFactory { + /** + * Creates a new MNIST loader with the default configuration. + * + * @return A new MNIST loader. + */ + actual fun create(): MNISTLoader { + return MNISTLoaderAndroid.create() + } + + /** + * Creates a new MNIST loader with a custom cache directory. + * + * @param cacheDir The directory to use for caching. + * @return A new MNIST loader. + */ + actual fun create(cacheDir: String): MNISTLoader { + return MNISTLoaderAndroid.create(cacheDir) + } + + /** + * Creates a new MNIST loader with a custom configuration. + * + * @param config The configuration to use. + * @return A new MNIST loader. + */ + actual fun create(config: MNISTLoaderConfig): MNISTLoader { + return MNISTLoaderAndroid.create(config) + } +} \ No newline at end of file diff --git a/model-zoo/src/commonMain/kotlin/sk/ai/net/io/data/mnist/MNISTData.kt b/model-zoo/src/commonMain/kotlin/sk/ai/net/io/data/mnist/MNISTData.kt new file mode 100644 index 00000000..011af91f --- /dev/null +++ b/model-zoo/src/commonMain/kotlin/sk/ai/net/io/data/mnist/MNISTData.kt @@ -0,0 +1,108 @@ +package sk.ai.net.io.data.mnist + +import kotlinx.serialization.Serializable + +/** + * Represents a single MNIST image with its label. + * + * @property image The pixel data of the image as a ByteArray (28x28 pixels). + * @property label The label of the image (0-9). + */ +@Serializable +data class MNISTImage( + val image: ByteArray, + val label: Byte +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as MNISTImage + + if (!image.contentEquals(other.image)) return false + if (label != other.label) return false + + return true + } + + override fun hashCode(): Int { + var result = image.contentHashCode() + result = 31 * result + label.toInt() + return result + } +} + +/** + * Represents a dataset of MNIST images. + * + * @property images The list of MNIST images. + */ +@Serializable +data class MNISTDataset( + val images: List +) { + /** + * Returns the number of images in the dataset. + */ + val size: Int + get() = images.size + + /** + * Returns a subset of the dataset. + * + * @param fromIndex The starting index (inclusive). + * @param toIndex The ending index (exclusive). + * @return A new MNISTDataset containing the specified range of images. + */ + fun subset(fromIndex: Int, toIndex: Int): MNISTDataset { + return MNISTDataset(images.subList(fromIndex, toIndex)) + } +} + +/** + * Configuration for the MNIST loader. + * + * @property cacheDir The directory where downloaded files will be cached. + * @property useCache Whether to use cached files if available. + */ +data class MNISTLoaderConfig( + val cacheDir: String = "mnist-data", + val useCache: Boolean = true +) + +/** + * Constants for the MNIST dataset. + */ +object MNISTConstants { + const val IMAGE_SIZE = 28 + const val IMAGE_PIXELS = IMAGE_SIZE * IMAGE_SIZE + + const val TRAIN_IMAGES_FILENAME = "train-images-idx3-ubyte.gz" + const val TRAIN_LABELS_FILENAME = "train-labels-idx1-ubyte.gz" + const val TEST_IMAGES_FILENAME = "t10k-images-idx3-ubyte.gz" + const val TEST_LABELS_FILENAME = "t10k-labels-idx1-ubyte.gz" + + const val TRAIN_IMAGES_URL = "https://ossci-datasets.s3.amazonaws.com/mnist/train-images-idx3-ubyte.gz" + const val TRAIN_LABELS_URL = "https://ossci-datasets.s3.amazonaws.com/mnist/train-labels-idx1-ubyte.gz" + const val TEST_IMAGES_URL = "https://ossci-datasets.s3.amazonaws.com/mnist/t10k-images-idx3-ubyte.gz" + const val TEST_LABELS_URL = "https://ossci-datasets.s3.amazonaws.com/mnist/t10k-labels-idx1-ubyte.gz" +} + +/** + * Interface for the MNIST loader. + */ +interface MNISTLoader { + /** + * Loads the MNIST training dataset. + * + * @return The MNIST training dataset. + */ + suspend fun loadTrainingData(): MNISTDataset + + /** + * Loads the MNIST test dataset. + * + * @return The MNIST test dataset. + */ + suspend fun loadTestData(): MNISTDataset +} \ No newline at end of file diff --git a/model-zoo/src/commonMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderCommon.kt b/model-zoo/src/commonMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderCommon.kt new file mode 100644 index 00000000..b53b1024 --- /dev/null +++ b/model-zoo/src/commonMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderCommon.kt @@ -0,0 +1,126 @@ +package sk.ai.net.io.data.mnist + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlin.io.encoding.ExperimentalEncodingApi + +/** + * Abstract base class for MNIST loaders that implements common functionality. + * + * @property config The configuration for the MNIST loader. + */ +abstract class MNISTLoaderCommon(val config: MNISTLoaderConfig) : MNISTLoader { + + /** + * Loads the MNIST training dataset. + * + * @return The MNIST training dataset. + */ + override suspend fun loadTrainingData(): MNISTDataset { + val imagesBytes = downloadAndCacheFile( + MNISTConstants.TRAIN_IMAGES_URL, + MNISTConstants.TRAIN_IMAGES_FILENAME + ) + val labelsBytes = downloadAndCacheFile( + MNISTConstants.TRAIN_LABELS_URL, + MNISTConstants.TRAIN_LABELS_FILENAME + ) + + return parseDataset(imagesBytes, labelsBytes) + } + + /** + * Loads the MNIST test dataset. + * + * @return The MNIST test dataset. + */ + override suspend fun loadTestData(): MNISTDataset { + val imagesBytes = downloadAndCacheFile( + MNISTConstants.TEST_IMAGES_URL, + MNISTConstants.TEST_IMAGES_FILENAME + ) + val labelsBytes = downloadAndCacheFile( + MNISTConstants.TEST_LABELS_URL, + MNISTConstants.TEST_LABELS_FILENAME + ) + + return parseDataset(imagesBytes, labelsBytes) + } + + /** + * Downloads and caches a file. + * + * @param url The URL to download from. + * @param filename The name of the file to save. + * @return The bytes of the decompressed file. + */ + protected abstract suspend fun downloadAndCacheFile(url: String, filename: String): ByteArray + + /** + * Parses the MNIST dataset from the images and labels files. + * + * @param imagesBytes The bytes of the images file. + * @param labelsBytes The bytes of the labels file. + * @return The parsed MNIST dataset. + */ + protected fun parseDataset(imagesBytes: ByteArray, labelsBytes: ByteArray): MNISTDataset { + // Parse images + val imagesMagic = readInt32(imagesBytes, 0) + if (imagesMagic != 2051) { + throw IllegalArgumentException("Invalid magic number for images file: $imagesMagic") + } + + val numImages = readInt32(imagesBytes, 4) + val numRows = readInt32(imagesBytes, 8) + val numCols = readInt32(imagesBytes, 12) + + if (numRows != MNISTConstants.IMAGE_SIZE || numCols != MNISTConstants.IMAGE_SIZE) { + throw IllegalArgumentException("Invalid image dimensions: $numRows x $numCols") + } + + // Parse labels + val labelsMagic = readInt32(labelsBytes, 0) + if (labelsMagic != 2049) { + throw IllegalArgumentException("Invalid magic number for labels file: $labelsMagic") + } + + val numLabels = readInt32(labelsBytes, 4) + + if (numImages != numLabels) { + throw IllegalArgumentException("Number of images ($numImages) does not match number of labels ($numLabels)") + } + + // Create dataset + val images = mutableListOf() + + for (i in 0 until numImages) { + val imageOffset = 16 + i * MNISTConstants.IMAGE_PIXELS + val labelOffset = 8 + i + + val image = ByteArray(MNISTConstants.IMAGE_PIXELS) + for (j in 0 until MNISTConstants.IMAGE_PIXELS) { + image[j] = imagesBytes[imageOffset + j] + } + + val label = labelsBytes[labelOffset] + + images.add(MNISTImage(image, label)) + } + + return MNISTDataset(images) + } + + /** + * Reads a 32-bit integer from a byte array in big-endian format. + * + * @param bytes The byte array. + * @param offset The offset to read from. + * @return The 32-bit integer. + */ + private fun readInt32(bytes: ByteArray, offset: Int): Int { + return ((bytes[offset].toInt() and 0xFF) shl 24) or + ((bytes[offset + 1].toInt() and 0xFF) shl 16) or + ((bytes[offset + 2].toInt() and 0xFF) shl 8) or + (bytes[offset + 3].toInt() and 0xFF) + } +} diff --git a/model-zoo/src/commonMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderFactory.kt b/model-zoo/src/commonMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderFactory.kt new file mode 100644 index 00000000..e27b35f3 --- /dev/null +++ b/model-zoo/src/commonMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderFactory.kt @@ -0,0 +1,29 @@ +package sk.ai.net.io.data.mnist + +/** + * Factory for creating MNIST loaders. + */ +expect object MNISTLoaderFactory { + /** + * Creates a new MNIST loader with the default configuration. + * + * @return A new MNIST loader. + */ + fun create(): MNISTLoader + + /** + * Creates a new MNIST loader with a custom cache directory. + * + * @param cacheDir The directory to use for caching. + * @return A new MNIST loader. + */ + fun create(cacheDir: String): MNISTLoader + + /** + * Creates a new MNIST loader with a custom configuration. + * + * @param config The configuration to use. + * @return A new MNIST loader. + */ + fun create(config: MNISTLoaderConfig): MNISTLoader +} \ No newline at end of file diff --git a/model-zoo/src/commonTest/kotlin/sk/ai/net/io/data/mnist/MNISTDataTest.kt b/model-zoo/src/commonTest/kotlin/sk/ai/net/io/data/mnist/MNISTDataTest.kt new file mode 100644 index 00000000..47c10051 --- /dev/null +++ b/model-zoo/src/commonTest/kotlin/sk/ai/net/io/data/mnist/MNISTDataTest.kt @@ -0,0 +1,106 @@ +package sk.ai.net.io.data.mnist + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals + +/** + * Tests for the MNIST data classes. + */ +class MNISTDataTest { + + /** + * Tests the MNISTImage class. + */ + @Test + fun testMNISTImage() { + // Create a test image + val image = ByteArray(MNISTConstants.IMAGE_PIXELS) { it.toByte() } + val label: Byte = 5 + val mnistImage = MNISTImage(image, label) + + // Verify the image + assertEquals(image, mnistImage.image) + assertEquals(label, mnistImage.label) + + // Test equals and hashCode + val sameImage = MNISTImage(image, label) + assertEquals(mnistImage, sameImage) + assertEquals(mnistImage.hashCode(), sameImage.hashCode()) + + // Test with different image + val differentImage = ByteArray(MNISTConstants.IMAGE_PIXELS) { (it + 1).toByte() } + val differentMnistImage1 = MNISTImage(differentImage, label) + assertNotEquals(mnistImage, differentMnistImage1) + assertNotEquals(mnistImage.hashCode(), differentMnistImage1.hashCode()) + + // Test with different label + val differentMnistImage2 = MNISTImage(image, 6) + assertNotEquals(mnistImage, differentMnistImage2) + assertNotEquals(mnistImage.hashCode(), differentMnistImage2.hashCode()) + } + + /** + * Tests the MNISTDataset class. + */ + @Test + fun testMNISTDataset() { + // Create test images + val images = List(10) { i -> + val image = ByteArray(MNISTConstants.IMAGE_PIXELS) { it.toByte() } + val label = i.toByte() + MNISTImage(image, label) + } + + // Create a dataset + val dataset = MNISTDataset(images) + + // Verify the dataset + assertEquals(images, dataset.images) + assertEquals(images.size, dataset.size) + + // Test subset + val subset = dataset.subset(2, 5) + assertEquals(3, subset.size) + assertEquals(images.subList(2, 5), subset.images) + } + + /** + * Tests the MNISTLoaderConfig class. + */ + @Test + fun testMNISTLoaderConfig() { + // Test default configuration + val defaultConfig = MNISTLoaderConfig() + assertEquals("mnist-data", defaultConfig.cacheDir) + assertEquals(true, defaultConfig.useCache) + + // Test custom configuration + val customConfig = MNISTLoaderConfig( + cacheDir = "custom-cache-dir", + useCache = false + ) + assertEquals("custom-cache-dir", customConfig.cacheDir) + assertEquals(false, customConfig.useCache) + } + + /** + * Tests the MNISTConstants object. + */ + @Test + fun testMNISTConstants() { + // Verify the constants + assertEquals(28, MNISTConstants.IMAGE_SIZE) + assertEquals(28 * 28, MNISTConstants.IMAGE_PIXELS) + + assertEquals("train-images-idx3-ubyte.gz", MNISTConstants.TRAIN_IMAGES_FILENAME) + assertEquals("train-labels-idx1-ubyte.gz", MNISTConstants.TRAIN_LABELS_FILENAME) + assertEquals("t10k-images-idx3-ubyte.gz", MNISTConstants.TEST_IMAGES_FILENAME) + assertEquals("t10k-labels-idx1-ubyte.gz", MNISTConstants.TEST_LABELS_FILENAME) + + assertEquals("https://ossci-datasets.s3.amazonaws.com/mnist/train-images-idx3-ubyte.gz", MNISTConstants.TRAIN_IMAGES_URL) + assertEquals("https://ossci-datasets.s3.amazonaws.com/mnist/train-labels-idx1-ubyte.gz", MNISTConstants.TRAIN_LABELS_URL) + assertEquals("https://ossci-datasets.s3.amazonaws.com/mnist/t10k-images-idx3-ubyte.gz", MNISTConstants.TEST_IMAGES_URL) + assertEquals("https://ossci-datasets.s3.amazonaws.com/mnist/t10k-labels-idx1-ubyte.gz", MNISTConstants.TEST_LABELS_URL) + } +} \ No newline at end of file diff --git a/model-zoo/src/iosMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderFactory.kt b/model-zoo/src/iosMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderFactory.kt new file mode 100644 index 00000000..d1f92402 --- /dev/null +++ b/model-zoo/src/iosMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderFactory.kt @@ -0,0 +1,35 @@ +package sk.ai.net.io.data.mnist + +/** + * iOS implementation of the MNIST loader factory. + */ +actual object MNISTLoaderFactory { + /** + * Creates a new MNIST loader with the default configuration. + * + * @return A new MNIST loader. + */ + actual fun create(): MNISTLoader { + return MNISTLoaderIos.create() + } + + /** + * Creates a new MNIST loader with a custom cache directory. + * + * @param cacheDir The directory to use for caching. + * @return A new MNIST loader. + */ + actual fun create(cacheDir: String): MNISTLoader { + return MNISTLoaderIos.create(cacheDir) + } + + /** + * Creates a new MNIST loader with a custom configuration. + * + * @param config The configuration to use. + * @return A new MNIST loader. + */ + actual fun create(config: MNISTLoaderConfig): MNISTLoader { + return MNISTLoaderIos.create(config) + } +} \ No newline at end of file diff --git a/model-zoo/src/iosMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderIos.kt b/model-zoo/src/iosMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderIos.kt new file mode 100644 index 00000000..5d10c807 --- /dev/null +++ b/model-zoo/src/iosMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderIos.kt @@ -0,0 +1,87 @@ +package sk.ai.net.io.data.mnist + +import io.ktor.client.HttpClient +import io.ktor.client.engine.darwin.Darwin +import io.ktor.client.request.get +import io.ktor.client.statement.HttpResponse +import io.ktor.client.call.body +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +/** + * iOS implementation of the MNIST loader. + * + * @property config The configuration for the MNIST loader. + */ +class MNISTLoaderIos(config: MNISTLoaderConfig) : MNISTLoaderCommon(config) { + + /** + * Downloads and caches a file. + * + * @param url The URL to download from. + * @param filename The name of the file to save. + * @return The bytes of the decompressed file. + */ + override suspend fun downloadAndCacheFile(url: String, filename: String): ByteArray = withContext(Dispatchers.Default) { + // In this simplified iOS implementation, we don't cache files + // We'll just download the file every time + println("Downloading file: $url") + val data = downloadFile(url) + + // Note: In a real implementation, we would use a native gzip library to decompress the data + // For this example, we're assuming the server provides uncompressed data for iOS clients + println("iOS implementation does not support gzip decompression in this example. Assuming data is already decompressed.") + + return@withContext data + } + + /** + * Downloads a file from a URL. + * + * @param url The URL to download from. + * @return The bytes of the file. + */ + private suspend fun downloadFile(url: String): ByteArray { + val client = HttpClient(Darwin) { + // No plugins needed for basic functionality + } + + try { + val httpResponse: HttpResponse = client.get(url) + return httpResponse.body() + } finally { + client.close() + } + } + + companion object { + /** + * Creates a new instance of MNISTLoaderIos with the default configuration. + * + * @return A new instance of MNISTLoaderIos. + */ + fun create(): MNISTLoaderIos { + return MNISTLoaderIos(MNISTLoaderConfig()) + } + + /** + * Creates a new instance of MNISTLoaderIos with a custom cache directory. + * + * @param cacheDir The directory to use for caching. + * @return A new instance of MNISTLoaderIos. + */ + fun create(cacheDir: String): MNISTLoaderIos { + return MNISTLoaderIos(MNISTLoaderConfig(cacheDir = cacheDir)) + } + + /** + * Creates a new instance of MNISTLoaderIos with a custom configuration. + * + * @param config The configuration to use. + * @return A new instance of MNISTLoaderIos. + */ + fun create(config: MNISTLoaderConfig): MNISTLoaderIos { + return MNISTLoaderIos(config) + } + } +} diff --git a/model-zoo/src/jvmMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoader.kt b/model-zoo/src/jvmMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoader.kt new file mode 100644 index 00000000..940a7478 --- /dev/null +++ b/model-zoo/src/jvmMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoader.kt @@ -0,0 +1,112 @@ +package sk.ai.net.io.data.mnist; + + + +import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.logging.Logging +import io.ktor.client.request.get +import io.ktor.client.request.headers +import io.ktor.utils.io.jvm.javaio.copyTo +import io.ktor.utils.io.core.use +import io.ktor.client.call.body +import io.ktor.client.plugins.onDownload +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsChannel +import io.ktor.utils.io.copyAndClose +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.util.zip.GZIPInputStream + +import kotlinx.coroutines.runBlocking +import java.nio.file.Files +import java.nio.file.Paths +import java.util.zip.GZIPOutputStream + + + + + + +import io.ktor.client.statement.HttpResponse + +suspend fun downloadFile(urlStr: String, outputPath: String) { + val client = HttpClient(CIO) { + install(Logging) + } + // This is the correct way to initialize the HttpClient with CIO + try { + val file = File(outputPath) + + val httpClient = HttpClient { + install(Logging) + } + + val httpResponse: HttpResponse = client.get(urlStr) { + onDownload { bytesSentTotal, contentLength -> + println("Received $bytesSentTotal bytes from $contentLength") + } + } + val responseBody: ByteArray = httpResponse.body() + file.writeBytes(responseBody) + println("A file saved to ${file.path}") + + } finally { + client.close() + } +} + + + + +fun decompressGzipFile(gzipFilePath: String, outputFilePath: String) { + GZIPInputStream(FileInputStream(gzipFilePath)).use { gzipInputStream -> + FileOutputStream(outputFilePath).use { outputStream -> + val buffer = ByteArray(1024) + var len: Int + while (gzipInputStream.read(buffer).also { len = it } > 0) { + outputStream.write(buffer, 0, len) + } + } + } + println("Decompressed $gzipFilePath to $outputFilePath") +} + + +fun downloadMnistDataset() = runBlocking { + val urls = mapOf( + "train-images-idx3-ubyte.gz" to "https://ossci-datasets.s3.amazonaws.com/mnist/train-images-idx3-ubyte.gz", + "train-labels-idx1-ubyte.gz" to "https://ossci-datasets.s3.amazonaws.com/mnist/train-labels-idx1-ubyte.gz", + "t10k-images-idx3-ubyte.gz" to "https://ossci-datasets.s3.amazonaws.com/mnist/t10k-images-idx3-ubyte.gz", + "t10k-labels-idx1-ubyte.gz" to "https://ossci-datasets.s3.amazonaws.com/mnist/t10k-labels-idx1-ubyte.gz" + ) + + // https://ossci-datasets.s3.amazonaws.com/mnist/train-images-idx3-ubyte.gz + + urls.forEach { (fileName, url) -> + val outputPath = Paths.get(fileName).toString() + if (!Files.exists(Paths.get(outputPath))) { + downloadFile(url, outputPath) + } else { + println("$fileName already exists. Skipping download.") + } + } +} + +fun decompress() { + val gzipFiles = listOf( + "train-images-idx3-ubyte.gz", + "train-labels-idx1-ubyte.gz", + "t10k-images-idx3-ubyte.gz", + "t10k-labels-idx1-ubyte.gz" + ) + + gzipFiles.forEach { gzipFile -> + val outputFile = gzipFile.removeSuffix(".gz") + decompressGzipFile(gzipFile, outputFile) + } +} + + diff --git a/model-zoo/src/jvmMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderFactory.kt b/model-zoo/src/jvmMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderFactory.kt new file mode 100644 index 00000000..53b35721 --- /dev/null +++ b/model-zoo/src/jvmMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderFactory.kt @@ -0,0 +1,35 @@ +package sk.ai.net.io.data.mnist + +/** + * JVM implementation of the MNIST loader factory. + */ +actual object MNISTLoaderFactory { + /** + * Creates a new MNIST loader with the default configuration. + * + * @return A new MNIST loader. + */ + actual fun create(): MNISTLoader { + return MNISTLoaderJvm.create() + } + + /** + * Creates a new MNIST loader with a custom cache directory. + * + * @param cacheDir The directory to use for caching. + * @return A new MNIST loader. + */ + actual fun create(cacheDir: String): MNISTLoader { + return MNISTLoaderJvm.create(cacheDir) + } + + /** + * Creates a new MNIST loader with a custom configuration. + * + * @param config The configuration to use. + * @return A new MNIST loader. + */ + actual fun create(config: MNISTLoaderConfig): MNISTLoader { + return MNISTLoaderJvm.create(config) + } +} \ No newline at end of file diff --git a/model-zoo/src/jvmMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderJvm.kt b/model-zoo/src/jvmMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderJvm.kt new file mode 100644 index 00000000..d61229d4 --- /dev/null +++ b/model-zoo/src/jvmMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderJvm.kt @@ -0,0 +1,140 @@ +package sk.ai.net.io.data.mnist + +import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.logging.Logging +import io.ktor.client.plugins.HttpTimeout +import io.ktor.client.request.get +import io.ktor.client.statement.HttpResponse +import io.ktor.client.call.body +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.util.zip.GZIPInputStream + +/** + * JVM implementation of the MNIST loader. + * + * @property config The configuration for the MNIST loader. + */ +class MNISTLoaderJvm(config: MNISTLoaderConfig) : MNISTLoaderCommon(config) { + + /** + * Downloads and caches a file. + * + * @param url The URL to download from. + * @param filename The name of the file to save. + * @return The bytes of the decompressed file. + */ + override suspend fun downloadAndCacheFile(url: String, filename: String): ByteArray = withContext(Dispatchers.IO) { + val cacheDir = File(config.cacheDir) + if (!cacheDir.exists()) { + cacheDir.mkdirs() + } + + val gzipFile = File(cacheDir, filename) + val decompressedFile = File(cacheDir, filename.removeSuffix(".gz")) + + // Check if the decompressed file already exists in cache + if (config.useCache && decompressedFile.exists()) { + println("Using cached file: ${decompressedFile.path}") + return@withContext decompressedFile.readBytes() + } + + // Check if the gzip file already exists in cache + if (!gzipFile.exists() || !config.useCache) { + println("Downloading file: $url") + downloadFile(url, gzipFile.path) + } else { + println("Using cached gzip file: ${gzipFile.path}") + } + + // Decompress the gzip file + println("Decompressing file: ${gzipFile.path}") + decompressGzipFile(gzipFile.path, decompressedFile.path) + + return@withContext decompressedFile.readBytes() + } + + /** + * Downloads a file from a URL. + * + * @param url The URL to download from. + * @param outputPath The path to save the file to. + */ + private suspend fun downloadFile(url: String, outputPath: String) { + val client = HttpClient(CIO) { + install(Logging) + + // Configure timeout for large files + install(HttpTimeout) { + requestTimeoutMillis = 60000 // 60 seconds + connectTimeoutMillis = 60000 // 60 seconds + socketTimeoutMillis = 60000 // 60 seconds + } + } + + try { + val file = File(outputPath) + + val httpResponse: HttpResponse = client.get(url) + val responseBody: ByteArray = httpResponse.body() + file.writeBytes(responseBody) + + println("File saved to ${file.path}") + } finally { + client.close() + } + } + + /** + * Decompresses a gzip file. + * + * @param gzipFilePath The path to the gzip file. + * @param outputFilePath The path to save the decompressed file to. + */ + private fun decompressGzipFile(gzipFilePath: String, outputFilePath: String) { + GZIPInputStream(FileInputStream(gzipFilePath)).use { gzipInputStream -> + FileOutputStream(outputFilePath).use { outputStream -> + val buffer = ByteArray(1024) + var len: Int + while (gzipInputStream.read(buffer).also { len = it } > 0) { + outputStream.write(buffer, 0, len) + } + } + } + } + + companion object { + /** + * Creates a new instance of MNISTLoaderJvm with the default configuration. + * + * @return A new instance of MNISTLoaderJvm. + */ + fun create(): MNISTLoaderJvm { + return MNISTLoaderJvm(MNISTLoaderConfig()) + } + + /** + * Creates a new instance of MNISTLoaderJvm with a custom cache directory. + * + * @param cacheDir The directory to use for caching. + * @return A new instance of MNISTLoaderJvm. + */ + fun create(cacheDir: String): MNISTLoaderJvm { + return MNISTLoaderJvm(MNISTLoaderConfig(cacheDir = cacheDir)) + } + + /** + * Creates a new instance of MNISTLoaderJvm with a custom configuration. + * + * @param config The configuration to use. + * @return A new instance of MNISTLoaderJvm. + */ + fun create(config: MNISTLoaderConfig): MNISTLoaderJvm { + return MNISTLoaderJvm(config) + } + } +} diff --git a/model-zoo/src/jvmTest/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderTest.kt b/model-zoo/src/jvmTest/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderTest.kt new file mode 100644 index 00000000..bd7c3589 --- /dev/null +++ b/model-zoo/src/jvmTest/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderTest.kt @@ -0,0 +1,130 @@ +package sk.ai.net.io.data.mnist + +import kotlinx.coroutines.runBlocking +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue +import java.io.File + +/** + * Tests for the MNIST loader. + */ +class MNISTLoaderTest { + + /** + * Tests loading the MNIST training dataset. + */ + @Test + fun testLoadTrainingData() = runBlocking { + // Create a temporary directory for caching + val tempDir = createTempDir("mnist-test") + try { + // Create a loader with the temporary directory + val loader = MNISTLoaderFactory.create(tempDir.absolutePath) + + // Load the training data + val dataset = loader.loadTrainingData() + + // Verify the dataset + assertNotNull(dataset) + assertTrue(dataset.size > 0) + assertEquals(60000, dataset.size) // MNIST training set has 60,000 images + + // Verify the first image + val firstImage = dataset.images[0] + assertNotNull(firstImage) + assertEquals(MNISTConstants.IMAGE_PIXELS, firstImage.image.size) + assertTrue(firstImage.label >= 0 && firstImage.label <= 9) + + // Verify that the cache files were created + val trainImagesFile = File(tempDir, MNISTConstants.TRAIN_IMAGES_FILENAME.removeSuffix(".gz")) + val trainLabelsFile = File(tempDir, MNISTConstants.TRAIN_LABELS_FILENAME.removeSuffix(".gz")) + assertTrue(trainImagesFile.exists()) + assertTrue(trainLabelsFile.exists()) + + // Load the data again to test caching + val cachedDataset = loader.loadTrainingData() + assertEquals(dataset.size, cachedDataset.size) + } finally { + // Clean up + tempDir.deleteRecursively() + } + } + + /** + * Tests loading the MNIST test dataset. + */ + @Test + fun testLoadTestData() = runBlocking { + // Create a temporary directory for caching + val tempDir = createTempDir("mnist-test") + try { + // Create a loader with the temporary directory + val loader = MNISTLoaderFactory.create(tempDir.absolutePath) + + // Load the test data + val dataset = loader.loadTestData() + + // Verify the dataset + assertNotNull(dataset) + assertTrue(dataset.size > 0) + assertEquals(10000, dataset.size) // MNIST test set has 10,000 images + + // Verify the first image + val firstImage = dataset.images[0] + assertNotNull(firstImage) + assertEquals(MNISTConstants.IMAGE_PIXELS, firstImage.image.size) + assertTrue(firstImage.label >= 0 && firstImage.label <= 9) + + // Verify that the cache files were created + val testImagesFile = File(tempDir, MNISTConstants.TEST_IMAGES_FILENAME.removeSuffix(".gz")) + val testLabelsFile = File(tempDir, MNISTConstants.TEST_LABELS_FILENAME.removeSuffix(".gz")) + assertTrue(testImagesFile.exists()) + assertTrue(testLabelsFile.exists()) + + // Load the data again to test caching + val cachedDataset = loader.loadTestData() + assertEquals(dataset.size, cachedDataset.size) + } finally { + // Clean up + tempDir.deleteRecursively() + } + } + + /** + * Tests the subset method of the MNIST dataset. + */ + @Test + fun testDatasetSubset() = runBlocking { + // Create a loader with the default configuration + val loader = MNISTLoaderFactory.create() + + // Load the training data + val dataset = loader.loadTrainingData() + + // Create a subset + val subset = dataset.subset(0, 100) + + // Verify the subset + assertEquals(100, subset.size) + assertEquals(dataset.images[0], subset.images[0]) + assertEquals(dataset.images[99], subset.images[99]) + } + + /** + * Tests the configuration of the MNIST loader. + */ + @Test + fun testLoaderConfiguration() { + // Create a loader with a custom configuration + val config = MNISTLoaderConfig( + cacheDir = "custom-cache-dir", + useCache = false + ) + val loader = MNISTLoaderFactory.create(config) + + // Verify that the loader was created successfully + assertNotNull(loader) + } +} \ No newline at end of file diff --git a/model-zoo/src/wasmJsMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderFactory.kt b/model-zoo/src/wasmJsMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderFactory.kt new file mode 100644 index 00000000..8b54972b --- /dev/null +++ b/model-zoo/src/wasmJsMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderFactory.kt @@ -0,0 +1,35 @@ +package sk.ai.net.io.data.mnist + +/** + * WASM JS implementation of the MNIST loader factory. + */ +actual object MNISTLoaderFactory { + /** + * Creates a new MNIST loader with the default configuration. + * + * @return A new MNIST loader. + */ + actual fun create(): MNISTLoader { + return MNISTLoaderWasmJs.create() + } + + /** + * Creates a new MNIST loader with a custom cache directory. + * + * @param cacheDir The directory to use for caching. + * @return A new MNIST loader. + */ + actual fun create(cacheDir: String): MNISTLoader { + return MNISTLoaderWasmJs.create(cacheDir) + } + + /** + * Creates a new MNIST loader with a custom configuration. + * + * @param config The configuration to use. + * @return A new MNIST loader. + */ + actual fun create(config: MNISTLoaderConfig): MNISTLoader { + return MNISTLoaderWasmJs.create(config) + } +} \ No newline at end of file diff --git a/model-zoo/src/wasmJsMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderWasmJs.kt b/model-zoo/src/wasmJsMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderWasmJs.kt new file mode 100644 index 00000000..357a285d --- /dev/null +++ b/model-zoo/src/wasmJsMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoaderWasmJs.kt @@ -0,0 +1,87 @@ +package sk.ai.net.io.data.mnist + +import io.ktor.client.HttpClient +import io.ktor.client.engine.js.Js +import io.ktor.client.request.get +import io.ktor.client.statement.HttpResponse +import io.ktor.client.call.body +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +/** + * WASM JS implementation of the MNIST loader. + * + * @property config The configuration for the MNIST loader. + */ +class MNISTLoaderWasmJs(config: MNISTLoaderConfig) : MNISTLoaderCommon(config) { + + /** + * Downloads and caches a file. + * + * @param url The URL to download from. + * @param filename The name of the file to save. + * @return The bytes of the decompressed file. + */ + override suspend fun downloadAndCacheFile(url: String, filename: String): ByteArray = withContext(Dispatchers.Default) { + // In WASM JS, we don't have file system access, so we can't cache files + // We'll just download the file every time + println("Downloading file: $url") + val data = downloadFile(url) + + // Note: In a real implementation, we would use a JS gzip library to decompress the data + // For this example, we're assuming the server provides uncompressed data for WASM clients + println("WASM JS implementation does not support gzip decompression. Assuming data is already decompressed.") + + return@withContext data + } + + /** + * Downloads a file from a URL. + * + * @param url The URL to download from. + * @return The bytes of the file. + */ + private suspend fun downloadFile(url: String): ByteArray { + val client = HttpClient(Js) { + // No plugins needed for basic functionality + } + + try { + val httpResponse: HttpResponse = client.get(url) + return httpResponse.body() + } finally { + client.close() + } + } + + companion object { + /** + * Creates a new instance of MNISTLoaderWasmJs with the default configuration. + * + * @return A new instance of MNISTLoaderWasmJs. + */ + fun create(): MNISTLoaderWasmJs { + return MNISTLoaderWasmJs(MNISTLoaderConfig()) + } + + /** + * Creates a new instance of MNISTLoaderWasmJs with a custom cache directory. + * + * @param cacheDir The directory to use for caching. + * @return A new instance of MNISTLoaderWasmJs. + */ + fun create(cacheDir: String): MNISTLoaderWasmJs { + return MNISTLoaderWasmJs(MNISTLoaderConfig(cacheDir = cacheDir)) + } + + /** + * Creates a new instance of MNISTLoaderWasmJs with a custom configuration. + * + * @param config The configuration to use. + * @return A new instance of MNISTLoaderWasmJs. + */ + fun create(config: MNISTLoaderConfig): MNISTLoaderWasmJs { + return MNISTLoaderWasmJs(config) + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 9ff9ff21..f7540043 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -17,4 +17,5 @@ rootProject.name = "skainet" include(":core") include(":io") include(":gguf") +include(":model-zoo") include(":samples:mnist-mlp-cli") From 2b0ea93515b46f596ff0772c6d82f8404570ed85 Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Sat, 31 May 2025 15:25:32 +0200 Subject: [PATCH 03/19] Add code for MNIST image classification. without training first. Related-To: #3 --- build.gradle.kts | 2 +- gradle/libs.versions.toml | 13 +- io/build.gradle.kts | 2 +- model-zoo/build.gradle.kts | 58 ++++++- samples/mnist-mlp-cli/build.gradle.kts | 8 +- .../net/samples/{sinus => mnist}/mlp/Main.kt | 0 .../sk/ai/net/samples/mnist/mlp/Main.kt | 162 ++++++++++++++---- 7 files changed, 196 insertions(+), 49 deletions(-) rename samples/mnist-mlp-cli/src/commonMain/kotlin/sk/ai/net/samples/{sinus => mnist}/mlp/Main.kt (100%) diff --git a/build.gradle.kts b/build.gradle.kts index 79b67b44..083ab137 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,7 +10,7 @@ plugins { allprojects { group = "sk.ai.net" - version = "0.0.5" + version = "0.0.6" } moduleGraphConfig { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 73ce053b..3aff7143 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,8 +5,9 @@ kotlinx-coroutines = "1.10.2" android-minSdk = "24" android-compileSdk = "35" kotlinxSerializationJson = "1.8.1" -nexus-publish = "2.0.0" -testng = "7.10.2" +ktorClientCore = "3.1.3" +ktorClientPlugins = "3.1.1" +logbackClassic = "1.5.18" binaryCompatibility = "0.17.0" moduleGraphSouza = "0.12.0" kotlinxIo = "0.7.0" @@ -15,8 +16,14 @@ kotlinxCli = "0.3.5" [libraries] kotlinx-coroutines = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } +kotlinx-coroutines-core-jvm = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm", version.ref = "kotlinxSerializationJson" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } -nexus-publish = { module = "io.github.gradle-nexus.publish-plugin:io.github.gradle-nexus.publish-plugin.gradle.plugin", version.ref = "nexus-publish" } +ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktorClientCore" } +ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktorClientCore" } +ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktorClientCore" } +ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktorClientCore" } +ktor-client-plugins = { module = "io.ktor:ktor-client-plugins", version.ref = "ktorClientPlugins" } +logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logbackClassic" } kotlinx-io-core = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version.ref = "kotlinxIo" } kotlinx-cli = { module = "org.jetbrains.kotlinx:kotlinx-cli", version.ref = "kotlinxCli" } diff --git a/io/build.gradle.kts b/io/build.gradle.kts index eee0ba64..6bc3befa 100644 --- a/io/build.gradle.kts +++ b/io/build.gradle.kts @@ -96,4 +96,4 @@ mavenPublishing { } } } -} +} \ No newline at end of file diff --git a/model-zoo/build.gradle.kts b/model-zoo/build.gradle.kts index d979c07d..0c8f1693 100644 --- a/model-zoo/build.gradle.kts +++ b/model-zoo/build.gradle.kts @@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.kotlinMultiplatform) id("com.android.library") - //alias(libs.plugins.vanniktech.mavenPublish) + alias(libs.plugins.vanniktech.mavenPublish) } kotlin { @@ -31,8 +31,8 @@ kotlin { implementation(project(":core")) implementation(libs.kotlinx.io.core) implementation(libs.kotlinx.serialization.json) - implementation("io.ktor:ktor-client-core:3.1.3") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1") + implementation(libs.ktor.client.core) + implementation(libs.kotlinx.coroutines) } } @@ -44,12 +44,12 @@ kotlin { val jvmMain by getting { dependencies { - implementation("io.ktor:ktor-client-cio:3.1.3") - implementation("io.ktor:ktor-client-plugins:3.1.1") - implementation("io.ktor:ktor-client-logging:3.1.3") - implementation("io.ktor:ktor-client-content-negotiation:3.1.3") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.8.1") - implementation("ch.qos.logback:logback-classic:1.4.14") // For logging + implementation(libs.ktor.client.cio) + implementation(libs.ktor.client.plugins) + implementation(libs.ktor.client.logging) + implementation(libs.ktor.client.content.negotiation) + implementation(libs.kotlinx.coroutines.core.jvm) + implementation(libs.logback.classic) // For logging } } @@ -93,3 +93,43 @@ android { minSdk = 24 } } + +publishing { + repositories { + maven { + name = "githubPackages" + url = uri("https://maven.pkg.github.com/sk-ai-net/skainet") + credentials { + credentials(PasswordCredentials::class) + } + } + } +} + +mavenPublishing { + + coordinates(group.toString(), "model-zoo", version.toString()) + + pom { + description.set("skainet") + name.set(project.name) + url.set("https://github.com/sk-ai-net/skainet/") + licenses { + license { + name.set("MIT") + distribution.set("repo") + } + } + scm { + url.set("https://github.com/sk-ai-net/skainet/") + connection.set("scm:git:git@github.com:sk-ai-net/skainet.git") + developerConnection.set("scm:git:ssh://git@github.com:sk-ai-net/skainet.git") + } + developers { + developer { + id.set("sk-ai-net") + name.set("sk-ai-net") + } + } + } +} diff --git a/samples/mnist-mlp-cli/build.gradle.kts b/samples/mnist-mlp-cli/build.gradle.kts index 8b506174..6b70fbf6 100644 --- a/samples/mnist-mlp-cli/build.gradle.kts +++ b/samples/mnist-mlp-cli/build.gradle.kts @@ -29,15 +29,21 @@ kotlin { implementation(project(":io")) implementation(project(":gguf")) implementation(project(":core")) + implementation(project(":model-zoo")) implementation(libs.kotlinx.io.core) implementation(libs.kotlinx.coroutines) implementation(libs.kotlinx.cli) + + implementation(libs.ktor.client.plugins) + implementation(libs.ktor.client.logging) + } } val jvmMain by getting { dependencies { + implementation(libs.ktor.client.cio) } } } @@ -50,7 +56,7 @@ tasks.register("jvmFatJar") { manifest { attributes( - "Main-Class" to "sk.ai.net.samples.sinus.mlp.MainKt" + "Main-Class" to "sk.ai.net.samples.mnist.mlp.MainKt" ) } diff --git a/samples/mnist-mlp-cli/src/commonMain/kotlin/sk/ai/net/samples/sinus/mlp/Main.kt b/samples/mnist-mlp-cli/src/commonMain/kotlin/sk/ai/net/samples/mnist/mlp/Main.kt similarity index 100% rename from samples/mnist-mlp-cli/src/commonMain/kotlin/sk/ai/net/samples/sinus/mlp/Main.kt rename to samples/mnist-mlp-cli/src/commonMain/kotlin/sk/ai/net/samples/mnist/mlp/Main.kt diff --git a/samples/mnist-mlp-cli/src/jvmMain/kotlin/sk/ai/net/samples/mnist/mlp/Main.kt b/samples/mnist-mlp-cli/src/jvmMain/kotlin/sk/ai/net/samples/mnist/mlp/Main.kt index b0211d66..dc669dfd 100644 --- a/samples/mnist-mlp-cli/src/jvmMain/kotlin/sk/ai/net/samples/mnist/mlp/Main.kt +++ b/samples/mnist-mlp-cli/src/jvmMain/kotlin/sk/ai/net/samples/mnist/mlp/Main.kt @@ -2,49 +2,85 @@ package sk.ai.net.samples.mnist.mlp import kotlinx.cli.ArgParser import kotlinx.cli.ArgType -import kotlinx.cli.required +import kotlinx.coroutines.runBlocking import sk.ai.net.Shape import sk.ai.net.Tensor import sk.ai.net.dsl.network import sk.ai.net.impl.DoublesTensor +import sk.ai.net.io.data.mnist.MNISTConstants +import sk.ai.net.io.data.mnist.MNISTImage +import sk.ai.net.io.data.mnist.MNISTLoaderFactory +import sk.ai.net.nn.Module import sk.ai.net.nn.activations.ReLU -import java.io.File /** - * Main entry point for the sinus-mlp-cli application. + * Main entry point for the mnist-mlp-cli application. * - * This application loads weights from a GGUF or SafeTensors file and uses an MLP network - * to approximate the sine function. + * This application creates an MLP network and uses it to classify MNIST handwritten digits. + * It loads the MNIST test data, classifies a subset of the images, and reports the accuracy. + * + * Usage: + * ``` + * ./gradlew :samples:mnist-mlp-cli:run --args="-m model.gguf -n 100" + * ``` + * + * Options: + * - `-m, --model`: Path to the model file (required, but not used in this simplified example) + * - `-n, --num-images`: Number of images to classify (default: 10) */ actual fun main(args: Array) { - val parser = ArgParser("sinus-mlp-cli") - val modelPath by parser.option( - ArgType.String, - shortName = "m", - fullName = "model", - description = "Path to the model file (GGUF or SafeTensors)" - ).required() + val parser = ArgParser("mnist-mlp-cli") + val numImagesArg by parser.option( + ArgType.Int, + shortName = "n", + fullName = "num-images", + description = "Number of images to classify (default: 10)" + ) + val numImages = numImagesArg ?: 10 parser.parse(args) println("MNIST MLP CLI") println("=============") - println("Model file: $modelPath") + println("Number of images to classify: $numImages") println() try { - // Check if the model file exists - val modelFile = File(modelPath) - if (!modelFile.exists()) { - println("Error: Model file not found: $modelPath") - return - } // In a real implementation, we would use ModelFormatLoader to load the model - // But for this simplified example, we'll create a simple MLP directly + // But for this simplified example, we'll create an MLP directly println("Creating MLP network...") - val mlp = createSimpleMLP() + val mlp = createMNISTMLP() + + // Load MNIST test data + println("Loading MNIST test data...") + val testData = runBlocking { + val loader = MNISTLoaderFactory.create() + loader.loadTestData() + } + println("Loaded ${testData.size} test images") + + // Classify a subset of the test images + val numImagesToClassify = minOf(numImages ?: 10, testData.size) + println("\nClassifying $numImagesToClassify images...") + + var correctPredictions = 0 + for (i in 0 until numImagesToClassify) { + val image = testData.images[i] + val prediction = classifyImage(mlp, image) + val actualLabel = image.label.toInt() + val isCorrect = prediction == actualLabel + if (isCorrect) { + correctPredictions++ + } + + println("Image ${i + 1}: Predicted $prediction, Actual $actualLabel ${if (isCorrect) "✓" else "✗"}") + } + + // Print accuracy + val accuracy = correctPredictions.toDouble() / numImagesToClassify * 100 + println("\nAccuracy: $correctPredictions/$numImagesToClassify (${accuracy.toInt()}%)") } catch (e: Exception) { println("Error: ${e.message}") @@ -53,35 +89,93 @@ actual fun main(args: Array) { } /** - * Creates a simple MLP network for sine approximation. + * Creates an MLP network for MNIST digit classification. + * + * The network architecture is as follows: + * - Input layer: 784 neurons (28x28 pixels flattened) + * - First hidden layer: 128 neurons with ReLU activation + * - Second hidden layer: 128 neurons with ReLU activation + * - Output layer: 10 neurons (one for each digit 0-9) * - * The network has one input (angle), two hidden layers with 10 neurons each, and one output neuron. + * This is a simple feedforward neural network that takes a flattened MNIST image as input + * and outputs a probability distribution over the 10 possible digits. The digit with the + * highest probability is the predicted digit. * * @return The MLP network. */ -private fun createSimpleMLP(): sk.ai.net.nn.Module { - // Create a context for the DSL +private fun createMNISTMLP(): sk.ai.net.nn.Module { // Create the MLP network using the DSL return network { - input(1) // One input neuron for the angle - dense(10) { // First hidden layer with 10 neurons + input(MNISTConstants.IMAGE_PIXELS) // 784 input neurons (28x28 pixels) + dense(128) { // First hidden layer with 128 neurons activation = ReLU()::invoke } - dense(10) { // Second hidden layer with 10 neurons + dense(128) { // Second hidden layer with 128 neurons activation = ReLU()::invoke } - dense(1) { // Output layer with 1 neuron + dense(10) { // Output layer with 10 neurons (one for each digit 0-9) // No activation for the output layer (linear) } } } /** - * Creates an input tensor for the given angle. + * Converts a MNIST image to a tensor suitable for input to the MLP network. * - * @param angle The angle in radians. - * @return A tensor representing the angle. + * The MNIST image is a 28x28 grayscale image stored as a ByteArray. + * This function: + * 1. Normalizes the pixel values from [0, 255] to [0, 1] + * 2. Flattens the 28x28 image into a 784-element vector + * 3. Creates a tensor with shape [784] + * + * @param image The MNIST image to convert. + * @return A tensor representing the image. */ -private fun createInputTensor(angle: Double): Tensor { - return DoublesTensor (Shape(1), doubleArrayOf(angle)) +private fun convertImageToTensor(image: MNISTImage): Tensor { + // Normalize pixel values to range [0, 1] + val normalizedPixels = DoubleArray(MNISTConstants.IMAGE_PIXELS) { i -> + (image.image[i].toInt() and 0xFF) / 255.0 + } + + // Create a tensor with shape [784] (flattened 28x28 image) + return DoublesTensor(Shape(MNISTConstants.IMAGE_PIXELS), normalizedPixels) +} + +/** + * Classifies a MNIST image using the given MLP network. + * + * This function: + * 1. Converts the MNIST image to a tensor + * 2. Passes the tensor through the MLP network + * 3. Finds the index of the maximum value in the output tensor + * 4. Returns the index as the predicted digit (0-9) + * + * The output tensor has 10 elements, one for each possible digit. + * The element with the highest value corresponds to the most likely digit. + * + * @param mlp The MLP network to use for classification. + * @param image The MNIST image to classify. + * @return The predicted digit (0-9). + */ +private fun classifyImage(mlp: Module, image: MNISTImage): Int { + // Convert the image to a tensor + val inputTensor = convertImageToTensor(image) + + // Forward pass through the network + val outputTensor = mlp.forward(inputTensor) as DoublesTensor + + // Find the index of the maximum value in the output tensor + // This is the predicted digit + val outputElements = outputTensor.elements + var maxIndex = 0 + var maxValue: Double = outputElements[0] + + for (i in 1 until outputElements.size) { + if (outputElements[i] > maxValue) { + maxValue = outputElements[i] + maxIndex = i + } + } + + return maxIndex } From 021328a2a5cd0497a8e33662701e251b2fe4d761 Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Sat, 31 May 2025 19:45:38 +0200 Subject: [PATCH 04/19] Initial version of MLP based MNIST is working Related-To: #3 --- .../kotlin/sk/ai/net/impl/DoublesTensor.kt | 12 +- .../kotlin/sk/ai/net/gguf/GGUFReader.kt | 5 +- .../sk/ai/net/samples/mnist/mlp/Main.kt | 146 +++++++++++++++--- 3 files changed, 135 insertions(+), 28 deletions(-) diff --git a/core/src/commonMain/kotlin/sk/ai/net/impl/DoublesTensor.kt b/core/src/commonMain/kotlin/sk/ai/net/impl/DoublesTensor.kt index 56c14661..588e7d5f 100644 --- a/core/src/commonMain/kotlin/sk/ai/net/impl/DoublesTensor.kt +++ b/core/src/commonMain/kotlin/sk/ai/net/impl/DoublesTensor.kt @@ -180,7 +180,15 @@ data class DoublesTensor(override val shape: Shape, val elements: DoubleArray) : matrixToString() } - else -> "Tensor(${shape}, ${elements.contentToString()})" // higher dimensions + else -> { + val values = if (elements.size < 100) { + elements.contentToString() + } else { + elements.slice(0..100).toDoubleArray().contentToString() + } + + "Tensor($shape, $values)" // higher dimensions + } } } @@ -265,7 +273,7 @@ data class DoublesTensor(override val shape: Shape, val elements: DoubleArray) : return DoublesTensor(newShape, result) } - throw IllegalArgumentException("Unsupported tensor shapes for multiplication.") + throw IllegalArgumentException("Unsupported tensor shapes for multiplication. $this $other") } override fun t(): Tensor { diff --git a/gguf/src/commonMain/kotlin/sk/ai/net/gguf/GGUFReader.kt b/gguf/src/commonMain/kotlin/sk/ai/net/gguf/GGUFReader.kt index 0c71d8d1..fb94970f 100644 --- a/gguf/src/commonMain/kotlin/sk/ai/net/gguf/GGUFReader.kt +++ b/gguf/src/commonMain/kotlin/sk/ai/net/gguf/GGUFReader.kt @@ -392,10 +392,7 @@ class GGUFReader(source: Source) { nElements = nElems.toInt(), nBytes = nBytes, dataOffset = dataOffs, - data = if (npDims.size == 1) tempData else tempData.reshape( - npDims[0].toInt(), - npDims[1].toInt() - ), + data = tempData, field = field ) ) diff --git a/samples/mnist-mlp-cli/src/jvmMain/kotlin/sk/ai/net/samples/mnist/mlp/Main.kt b/samples/mnist-mlp-cli/src/jvmMain/kotlin/sk/ai/net/samples/mnist/mlp/Main.kt index dc669dfd..664cef52 100644 --- a/samples/mnist-mlp-cli/src/jvmMain/kotlin/sk/ai/net/samples/mnist/mlp/Main.kt +++ b/samples/mnist-mlp-cli/src/jvmMain/kotlin/sk/ai/net/samples/mnist/mlp/Main.kt @@ -2,34 +2,52 @@ package sk.ai.net.samples.mnist.mlp import kotlinx.cli.ArgParser import kotlinx.cli.ArgType +import kotlinx.cli.required import kotlinx.coroutines.runBlocking +import kotlinx.io.asSource +import kotlinx.io.buffered import sk.ai.net.Shape import sk.ai.net.Tensor import sk.ai.net.dsl.network +import sk.ai.net.gguf.GGMLQuantizationType +import sk.ai.net.gguf.GGUFReader import sk.ai.net.impl.DoublesTensor import sk.ai.net.io.data.mnist.MNISTConstants import sk.ai.net.io.data.mnist.MNISTImage import sk.ai.net.io.data.mnist.MNISTLoaderFactory +import sk.ai.net.nn.Linear import sk.ai.net.nn.Module import sk.ai.net.nn.activations.ReLU +import java.io.File +import java.io.FileInputStream +import java.util.function.Supplier +import java.util.stream.Collectors + /** * Main entry point for the mnist-mlp-cli application. - * - * This application creates an MLP network and uses it to classify MNIST handwritten digits. + * + * This application creates an MLP network, loads model weights from a GGUF file, + * and uses the model to classify MNIST handwritten digits. * It loads the MNIST test data, classifies a subset of the images, and reports the accuracy. - * + * * Usage: * ``` * ./gradlew :samples:mnist-mlp-cli:run --args="-m model.gguf -n 100" * ``` - * + * * Options: - * - `-m, --model`: Path to the model file (required, but not used in this simplified example) + * - `-m, --model`: Path to the model file in GGUF format (required) * - `-n, --num-images`: Number of images to classify (default: 10) */ actual fun main(args: Array) { val parser = ArgParser("mnist-mlp-cli") + val modelPath by parser.option( + ArgType.String, + shortName = "m", + fullName = "model", + description = "Path to the model file (GGUF format)" + ).required() val numImagesArg by parser.option( ArgType.Int, shortName = "n", @@ -42,16 +60,19 @@ actual fun main(args: Array) { println("MNIST MLP CLI") println("=============") + println("Model file: $modelPath") println("Number of images to classify: $numImages") println() try { - - // In a real implementation, we would use ModelFormatLoader to load the model - // But for this simplified example, we'll create an MLP directly + // Create the MLP network println("Creating MLP network...") val mlp = createMNISTMLP() + // Load model weights from GGUF file + println("Loading model weights from $modelPath...") + loadModelWeights(mlp, modelPath) + // Load MNIST test data println("Loading MNIST test data...") val testData = runBlocking { @@ -90,30 +111,27 @@ actual fun main(args: Array) { /** * Creates an MLP network for MNIST digit classification. - * + * * The network architecture is as follows: * - Input layer: 784 neurons (28x28 pixels flattened) * - First hidden layer: 128 neurons with ReLU activation * - Second hidden layer: 128 neurons with ReLU activation * - Output layer: 10 neurons (one for each digit 0-9) - * + * * This is a simple feedforward neural network that takes a flattened MNIST image as input * and outputs a probability distribution over the 10 possible digits. The digit with the * highest probability is the predicted digit. - * + * * @return The MLP network. */ -private fun createMNISTMLP(): sk.ai.net.nn.Module { +private fun createMNISTMLP(): Module { // Create the MLP network using the DSL return network { input(MNISTConstants.IMAGE_PIXELS) // 784 input neurons (28x28 pixels) - dense(128) { // First hidden layer with 128 neurons + dense(128, "hidden1") { // First hidden layer with 128 neurons activation = ReLU()::invoke } - dense(128) { // Second hidden layer with 128 neurons - activation = ReLU()::invoke - } - dense(10) { // Output layer with 10 neurons (one for each digit 0-9) + dense(10, "output") { // Output layer with 10 neurons (one for each digit 0-9) // No activation for the output layer (linear) } } @@ -121,13 +139,13 @@ private fun createMNISTMLP(): sk.ai.net.nn.Module { /** * Converts a MNIST image to a tensor suitable for input to the MLP network. - * + * * The MNIST image is a 28x28 grayscale image stored as a ByteArray. * This function: * 1. Normalizes the pixel values from [0, 255] to [0, 1] * 2. Flattens the 28x28 image into a 784-element vector * 3. Creates a tensor with shape [784] - * + * * @param image The MNIST image to convert. * @return A tensor representing the image. */ @@ -141,18 +159,102 @@ private fun convertImageToTensor(image: MNISTImage): Tensor { return DoublesTensor(Shape(MNISTConstants.IMAGE_PIXELS), normalizedPixels) } +/** + * Loads model weights from a GGUF file and applies them to the MLP network. + * + * This function: + * 1. Opens the GGUF file + * 2. Extracts the tensors + * 3. Applies them to the MLP network + * + * @param mlp The MLP network to apply the weights to. + * @param modelPath The path to the GGUF file. + */ +private fun loadModelWeights(mlp: Module, modelPath: String) { + // Open the GGUF file + val inputStream = FileInputStream(File(modelPath)) + + // Create a GGUFReader to parse the file + val reader = GGUFReader(inputStream.asSource().buffered()) + + // Get the tensors from the GGUF file + val tensors = reader.tensors + + println("Found ${tensors.size} tensors in the GGUF file") + + // Apply the tensors to the MLP network + // The MLP network has the following structure: + // - Input layer + // - Linear layer 1 (784 -> 128) + ReLU + // - Linear layer 2 (128 -> 128) + ReLU + // - Linear layer 3 (128 -> 10) + + // Get the linear layers from the MLP + val linearLayers = mutableListOf() + fun findLinearLayers(module: Module) { + if (module is Linear) { + linearLayers.add(module) + } + for (child in module.modules) { + findLinearLayers(child) + } + } + findLinearLayers(mlp) + + for (layer in linearLayers) { + val name = layer.name + println("Processing tensor: $name") + } + + + println("Found ${linearLayers.size} linear layers in the MLP network") + + // Map the tensors to the linear layers + for (tensor in tensors) { + val name = tensor.name.substring("model.".length) + println("Processing tensor: $name") + + // Find the corresponding linear layer and parameter + for (layer in linearLayers) { + for (param in layer.params) { + if (param.name.contains(name, ignoreCase = true)) { + // Convert the tensor data to a DoublesTensor + val tensor = when (tensor.tensorType) { + GGMLQuantizationType.F32, + GGMLQuantizationType.F16 -> { + val floatsList = tensor.data as List + DoublesTensor( + param.value.shape, + floatsList.map { it.toDouble() }.toDoubleArray() + ) + } + else -> { + println("Unsupported tensor data type: ${tensor.data.firstOrNull()?.javaClass}") + continue + } + } + + // Apply the tensor to the parameter + param.value = tensor + println("Applied tensor $name to parameter ${param.name}") + } + } + } + } +} + /** * Classifies a MNIST image using the given MLP network. - * + * * This function: * 1. Converts the MNIST image to a tensor * 2. Passes the tensor through the MLP network * 3. Finds the index of the maximum value in the output tensor * 4. Returns the index as the predicted digit (0-9) - * + * * The output tensor has 10 elements, one for each possible digit. * The element with the highest value corresponds to the most likely digit. - * + * * @param mlp The MLP network to use for classification. * @param image The MNIST image to classify. * @return The predicted digit (0-9). From 47d134153232bc16f42af6d82c600a7788151c2a Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Sat, 31 May 2025 22:27:46 +0200 Subject: [PATCH 05/19] Initial version of MLP based MNIST is working Related-To: #3 Requires: https://github.com/sk-ai-net/skainet/issues/2 --- .../src/jvmMain/kotlin/sk/ai/net/samples/mnist/mlp/Main.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/samples/mnist-mlp-cli/src/jvmMain/kotlin/sk/ai/net/samples/mnist/mlp/Main.kt b/samples/mnist-mlp-cli/src/jvmMain/kotlin/sk/ai/net/samples/mnist/mlp/Main.kt index 664cef52..65abbc25 100644 --- a/samples/mnist-mlp-cli/src/jvmMain/kotlin/sk/ai/net/samples/mnist/mlp/Main.kt +++ b/samples/mnist-mlp-cli/src/jvmMain/kotlin/sk/ai/net/samples/mnist/mlp/Main.kt @@ -20,8 +20,6 @@ import sk.ai.net.nn.Module import sk.ai.net.nn.activations.ReLU import java.io.File import java.io.FileInputStream -import java.util.function.Supplier -import java.util.stream.Collectors /** From 0a9373512068e9b99db88583624a326da7a13b5c Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Sun, 8 Jun 2025 23:15:06 +0200 Subject: [PATCH 06/19] Initial version of fllaten Related-To: #26 --- .../commonMain/kotlin/sk/ai/net/nn/Flatten.kt | 20 +++++ .../kotlin/sk/ai/net/nn/FlattenTest.kt | 77 +++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 core/src/commonMain/kotlin/sk/ai/net/nn/Flatten.kt create mode 100644 core/src/commonTest/kotlin/sk/ai/net/nn/FlattenTest.kt diff --git a/core/src/commonMain/kotlin/sk/ai/net/nn/Flatten.kt b/core/src/commonMain/kotlin/sk/ai/net/nn/Flatten.kt new file mode 100644 index 00000000..4e7852d9 --- /dev/null +++ b/core/src/commonMain/kotlin/sk/ai/net/nn/Flatten.kt @@ -0,0 +1,20 @@ +package sk.ai.net.nn + +import sk.ai.net.Tensor + +/** + * A simple layer that flattens an input tensor into a 1D tensor. + * This layer has no parameters and simply reshapes the input. + */ +class Flatten( + private val startDim: Int = 1, + private val endDim: Int = -1, + override val name: String = "Flatten" +) : Module() { + override val modules: List + get() = emptyList() + + override fun forward(input: Tensor): Tensor { + return input.flatten(startDim, endDim) + } +} diff --git a/core/src/commonTest/kotlin/sk/ai/net/nn/FlattenTest.kt b/core/src/commonTest/kotlin/sk/ai/net/nn/FlattenTest.kt new file mode 100644 index 00000000..e88c76cc --- /dev/null +++ b/core/src/commonTest/kotlin/sk/ai/net/nn/FlattenTest.kt @@ -0,0 +1,77 @@ +package sk.ai.net.nn + +import sk.ai.net.Shape +import sk.ai.net.impl.DoublesTensor +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals + +class FlattenTest { + @Test + fun `flatten 2d tensor`() { + val tensor = DoublesTensor( + Shape(2, 3), + doubleArrayOf(1.0, 2.0, 3.0, 4.0, 5.0, 6.0) + ) + val flatten = Flatten() + val result = flatten.forward(tensor) as DoublesTensor + assertEquals(Shape(6), result.shape) + assertContentEquals(doubleArrayOf(1.0, 2.0, 3.0, 4.0, 5.0, 6.0), result.elements) + } + + @Test + fun `flatten 3d tensor`() { + val tensor = DoublesTensor( + Shape(2, 2, 2), + doubleArrayOf(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0) + ) + val flatten = Flatten() + val result = flatten.forward(tensor) as DoublesTensor + assertEquals(Shape(8), result.shape) + assertContentEquals( + doubleArrayOf(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0), + result.elements + ) + } + + @Test + fun flatten_basic() { + val flatten = Flatten() + val input = DoublesTensor(Shape(2,1,28,28), DoubleArray(2*1*28*28)) + val output = flatten.forward(input) as DoublesTensor + assertEquals(Shape(2,784), output.shape) + } + + @Test + fun flatten_with_custom_start_dim() { + val flatten = Flatten(startDim = 1) + val input = DoublesTensor(Shape(2,3,4), DoubleArray(2*3*4)) + val output = flatten.forward(input) as DoublesTensor + assertEquals(Shape(2,12), output.shape) + } + + @Test + fun flatten_single_sample() { + val flatten = Flatten() + val input = DoublesTensor(Shape(1,3,3), DoubleArray(1*3*3)) + val output = flatten.forward(input) as DoublesTensor + assertEquals(Shape(1,9), output.shape) + } + + @Test + fun flatten_preserve_batch_dim() { + val flatten = Flatten() + val input = DoublesTensor(Shape(10,5,2,2), DoubleArray(10*5*2*2)) + val output = flatten.forward(input) as DoublesTensor + assertEquals(Shape(10,20), output.shape) + } + + @Test + fun flatten_no_batch_dim() { + val flatten = Flatten() + val input = DoublesTensor(Shape(1,3,4), DoubleArray(3*4)) + val output = flatten.forward(input) as DoublesTensor + assertEquals(Shape(1,12), output.shape) + } +} From aa8c70890e276fed9785b5c084aaf9f05d6b53d5 Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Sun, 8 Jun 2025 23:16:51 +0200 Subject: [PATCH 07/19] Initial version of conv2d Related-To: #4 --- .../commonMain/kotlin/sk/ai/net/nn/Conv2d.kt | 158 +++++++++++ .../kotlin/sk/ai/net/nn/Conv2dTest.kt | 268 ++++++++++++++++++ 2 files changed, 426 insertions(+) create mode 100644 core/src/commonMain/kotlin/sk/ai/net/nn/Conv2d.kt create mode 100644 core/src/commonTest/kotlin/sk/ai/net/nn/Conv2dTest.kt diff --git a/core/src/commonMain/kotlin/sk/ai/net/nn/Conv2d.kt b/core/src/commonMain/kotlin/sk/ai/net/nn/Conv2d.kt new file mode 100644 index 00000000..dfca5c9d --- /dev/null +++ b/core/src/commonMain/kotlin/sk/ai/net/nn/Conv2d.kt @@ -0,0 +1,158 @@ +package sk.ai.net.nn + +import sk.ai.net.Shape +import sk.ai.net.Tensor +import sk.ai.net.impl.DoublesTensor +import sk.ai.net.rand +import sk.ai.net.zeros +import kotlin.math.sqrt + +class Conv2d( + val inChannels: Int, + val outChannels: Int, + val kernelSize: Int, + val stride: Int = 1, + val padding: Int = 0, + useBias: Boolean = true, + name: String = "Conv2d" +) : Module() { + override val name: String = name + val weight: Tensor + val bias: Tensor? + override val modules: List + get() = emptyList() + + override fun forward(input: Tensor): Tensor = con2d(input) + + init { + // Initialize weights and bias + val fanIn = inChannels * kernelSize * kernelSize + val bound = 1f / sqrt(fanIn.toDouble()).toFloat() // 1/sqrt(fanIn) + // Weight: uniform in [-bound, bound] + weight = (((rand( + Shape( + outChannels, + inChannels, + kernelSize, + kernelSize + ) + ) as DoublesTensor) * (2f * bound).toDouble()) as DoublesTensor) - bound.toDouble() + // Bias: uniform in [-bound, bound] if enabled + bias = if (useBias) { + ((rand(Shape(outChannels)) as DoublesTensor) * (2f * bound).toDouble()) - bound.toDouble() + } else { + null + } + } + + fun con2d(input: Tensor): Tensor { + // Ensure input has 3D or 4D shape + val shape = input.shape // assume shape is a list or array of dimensions + require(shape.rank == 3 || shape.rank == 4) { + "Conv2d expected 3D or 4D input tensor, but got shape ${shape}." + } + // Determine batch size and input dims + val batchSize: Int + val inC: Int + val inH: Int + val inW: Int + if (shape.rank == 4) { + batchSize = shape.dimensions[0] + inC = shape[1] + inH = shape[2] + inW = shape[3] + } else { + // if 3D (C, H, W), treat as batch of size 1 + batchSize = 1 + inC = shape[0] + inH = shape[1] + inW = shape[2] + } + require(inC == inChannels) { + "Conv2d expected input channel count $inChannels, but got $inC." + } + + // Compute output spatial dimensions + val outH = (inH + 2 * padding - kernelSize) / stride + 1 + val outW = (inW + 2 * padding - kernelSize) / stride + 1 + require(outH > 0 && outW > 0) { + "Conv2d output size is invalid (outH=$outH, outW=$outW). Check input dimensions and padding." + } + + // Cast input to DoublesTensor for easier access + val inputTensor = input as DoublesTensor + + // Apply padding if needed + val paddedInput: DoublesTensor = if (padding > 0) { + val paddedH = inH + 2 * padding + val paddedW = inW + 2 * padding + val paddedSize = batchSize * inC * paddedH * paddedW + val paddedElements = DoubleArray(paddedSize) { 0.0 } + + // Calculate strides for the padded tensor + val paddedStrides = IntArray(4) + paddedStrides[3] = 1 + paddedStrides[2] = paddedW + paddedStrides[1] = paddedH * paddedW + paddedStrides[0] = inC * paddedH * paddedW + + // Fill the padded tensor with the input values + for (n in 0 until batchSize) { + for (c in 0 until inC) { + for (i in 0 until inH) { + for (j in 0 until inW) { + val paddedIdx = n * paddedStrides[0] + c * paddedStrides[1] + + (i + padding) * paddedStrides[2] + (j + padding) * paddedStrides[3] + + val inputValue = if (shape.rank == 4) { + inputTensor[n, c, i, j] + } else { + inputTensor[c, i, j] + } + + paddedElements[paddedIdx] = inputValue + } + } + } + } + + DoublesTensor(Shape(batchSize, inC, paddedH, paddedW), paddedElements) + } else { + inputTensor // no padding needed + } + + // Prepare output tensor + val outputElements = DoubleArray(batchSize * outChannels * outH * outW) + var idx = 0 + + // Cast weight and bias to DoublesTensor for easier access + val weightTensor = weight as DoublesTensor + val biasTensor = bias as? DoublesTensor + + // Convolution: iterate over batch, out channels, and output spatial positions + for (n in 0 until batchSize) { + for (oc in 0 until outChannels) { + val biasVal = if (biasTensor != null) biasTensor[oc] else 0.0 + for (i in 0 until outH) { + for (j in 0 until outW) { + var sum = 0.0 + // Sum over all input channels and kernel elements + for (c in 0 until inChannels) { + for (ki in 0 until kernelSize) { + for (kj in 0 until kernelSize) { + val h = i * stride + ki + val w = j * stride + kj + sum += paddedInput[n, c, h, w] * weightTensor[oc, c, ki, kj] + } + } + } + // Add bias and assign to output + outputElements[idx++] = sum + biasVal + } + } + } + } + + return DoublesTensor(Shape(batchSize, outChannels, outH, outW), outputElements) + } +} diff --git a/core/src/commonTest/kotlin/sk/ai/net/nn/Conv2dTest.kt b/core/src/commonTest/kotlin/sk/ai/net/nn/Conv2dTest.kt new file mode 100644 index 00000000..8d829272 --- /dev/null +++ b/core/src/commonTest/kotlin/sk/ai/net/nn/Conv2dTest.kt @@ -0,0 +1,268 @@ +package sk.ai.net.nn + +import sk.ai.net.Shape +import sk.ai.net.impl.DoublesTensor +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class Conv2dTest { + @Test + fun testBasicConv2d() { + // Create a simple 1x1x3x3 input tensor (1 batch, 1 channel, 3x3 spatial dimensions) + val input = DoublesTensor( + Shape(1, 1, 3, 3), + doubleArrayOf( + 1.0, 2.0, 3.0, + 4.0, 5.0, 6.0, + 7.0, 8.0, 9.0 + ) + ) + + // Create a Conv2d layer with 1 input channel, 1 output channel, 2x2 kernel, no stride, no padding + val conv = Conv2d( + inChannels = 1, + outChannels = 1, + kernelSize = 2, + stride = 1, + padding = 0, + useBias = true + ) + + // Apply convolution + val result = conv.forward(input) as DoublesTensor + + // Expected output shape: 1x1x2x2 (1 batch, 1 channel, 2x2 spatial dimensions) + assertEquals(Shape(1, 1, 2, 2), result.shape) + + // Since we can't control the weights, we can't check exact values + // But we can check that the output has the right shape and contains non-zero values + assertTrue(result.elements.any { it != 0.0 }, "Output should contain non-zero values") + } + + @Test + fun testConv2dWithStride() { + // Create a simple 1x1x4x4 input tensor + val input = DoublesTensor( + Shape(1, 1, 4, 4), + doubleArrayOf( + 1.0, 2.0, 3.0, 4.0, + 5.0, 6.0, 7.0, 8.0, + 9.0, 10.0, 11.0, 12.0, + 13.0, 14.0, 15.0, 16.0 + ) + ) + + // Create a Conv2d layer with stride=2 + val conv = Conv2d( + inChannels = 1, + outChannels = 1, + kernelSize = 2, + stride = 2, + padding = 0, + useBias = true + ) + + // Apply convolution + val result = conv.forward(input) as DoublesTensor + + // Expected output shape: 1x1x2x2 (stride=2 reduces spatial dimensions by half) + assertEquals(Shape(1, 1, 2, 2), result.shape) + + // Check that the output contains non-zero values + assertTrue(result.elements.any { it != 0.0 }, "Output should contain non-zero values") + } + + @Test + fun testConv2dWithPadding() { + // Create a simple 1x1x3x3 input tensor + val input = DoublesTensor( + Shape(1, 1, 3, 3), + doubleArrayOf( + 1.0, 2.0, 3.0, + 4.0, 5.0, 6.0, + 7.0, 8.0, 9.0 + ) + ) + + // Create a Conv2d layer with padding=1 + val conv = Conv2d( + inChannels = 1, + outChannels = 1, + kernelSize = 3, + stride = 1, + padding = 1, + useBias = true + ) + + // Apply convolution + val result = conv.forward(input) as DoublesTensor + + // Expected output shape: 1x1x3x3 (padding preserves spatial dimensions) + assertEquals(Shape(1, 1, 3, 3), result.shape) + + // Check that the output contains non-zero values + assertTrue(result.elements.any { it != 0.0 }, "Output should contain non-zero values") + } + + @Test + fun testConv2dMultipleChannels() { + // Create a 1x2x3x3 input tensor (1 batch, 2 channels, 3x3 spatial dimensions) + val input = DoublesTensor( + Shape(1, 2, 3, 3), + doubleArrayOf( + // Channel 1 + 1.0, 2.0, 3.0, + 4.0, 5.0, 6.0, + 7.0, 8.0, 9.0, + // Channel 2 + 9.0, 8.0, 7.0, + 6.0, 5.0, 4.0, + 3.0, 2.0, 1.0 + ) + ) + + // Create a Conv2d layer with 2 input channels, 1 output channel + val conv = Conv2d( + inChannels = 2, + outChannels = 1, + kernelSize = 2, + stride = 1, + padding = 0, + useBias = true + ) + + // Apply convolution + val result = conv.forward(input) as DoublesTensor + + // Expected output shape: 1x1x2x2 + assertEquals(Shape(1, 1, 2, 2), result.shape) + + // Check that the output contains non-zero values + assertTrue(result.elements.any { it != 0.0 }, "Output should contain non-zero values") + } + + @Test + fun testConv2dMultipleOutputChannels() { + // Create a 1x1x3x3 input tensor + val input = DoublesTensor( + Shape(1, 1, 3, 3), + doubleArrayOf( + 1.0, 2.0, 3.0, + 4.0, 5.0, 6.0, + 7.0, 8.0, 9.0 + ) + ) + + // Create a Conv2d layer with 1 input channel, 2 output channels + val conv = Conv2d( + inChannels = 1, + outChannels = 2, + kernelSize = 2, + stride = 1, + padding = 0, + useBias = true + ) + + // Apply convolution + val result = conv.forward(input) as DoublesTensor + + // Expected output shape: 1x2x2x2 (1 batch, 2 output channels, 2x2 spatial dimensions) + assertEquals(Shape(1, 2, 2, 2), result.shape) + + // Check that the output contains non-zero values + assertTrue(result.elements.any { it != 0.0 }, "Output should contain non-zero values") + } + + @Test + fun testConv2dBatchProcessing() { + // Create a 2x1x3x3 input tensor (2 batches, 1 channel, 3x3 spatial dimensions) + val input = DoublesTensor( + Shape(2, 1, 3, 3), + doubleArrayOf( + // Batch 1 + 1.0, 2.0, 3.0, + 4.0, 5.0, 6.0, + 7.0, 8.0, 9.0, + // Batch 2 + 9.0, 8.0, 7.0, + 6.0, 5.0, 4.0, + 3.0, 2.0, 1.0 + ) + ) + + // Create a Conv2d layer + val conv = Conv2d( + inChannels = 1, + outChannels = 1, + kernelSize = 2, + stride = 1, + padding = 0, + useBias = true + ) + + // Apply convolution + val result = conv.forward(input) as DoublesTensor + + // Expected output shape: 2x1x2x2 (2 batches, 1 channel, 2x2 spatial dimensions) + assertEquals(Shape(2, 1, 2, 2), result.shape) + + // Check that the output contains non-zero values + assertTrue(result.elements.any { it != 0.0 }, "Output should contain non-zero values") + } + + @Test + fun testConv2dOutputSizeCalculation() { + // Test various combinations of input size, kernel size, stride, and padding + // to verify that the output size is calculated correctly + + // Case 1: No padding, stride 1 + val input1 = DoublesTensor(Shape(1, 1, 5, 5), DoubleArray(25) { 1.0 }) + val conv1 = Conv2d(inChannels = 1, outChannels = 1, kernelSize = 3, stride = 1, padding = 0) + val result1 = conv1.forward(input1) as DoublesTensor + assertEquals(Shape(1, 1, 3, 3), result1.shape) // (5 - 3) / 1 + 1 = 3 + + // Case 2: With padding, stride 1 + val input2 = DoublesTensor(Shape(1, 1, 5, 5), DoubleArray(25) { 1.0 }) + val conv2 = Conv2d(inChannels = 1, outChannels = 1, kernelSize = 3, stride = 1, padding = 1) + val result2 = conv2.forward(input2) as DoublesTensor + assertEquals(Shape(1, 1, 5, 5), result2.shape) // (5 + 2*1 - 3) / 1 + 1 = 5 + + // Case 3: No padding, stride 2 + val input3 = DoublesTensor(Shape(1, 1, 5, 5), DoubleArray(25) { 1.0 }) + val conv3 = Conv2d(inChannels = 1, outChannels = 1, kernelSize = 3, stride = 2, padding = 0) + val result3 = conv3.forward(input3) as DoublesTensor + assertEquals(Shape(1, 1, 2, 2), result3.shape) // (5 - 3) / 2 + 1 = 2 + + // Case 4: With padding, stride 2 + val input4 = DoublesTensor(Shape(1, 1, 5, 5), DoubleArray(25) { 1.0 }) + val conv4 = Conv2d(inChannels = 1, outChannels = 1, kernelSize = 3, stride = 2, padding = 1) + val result4 = conv4.forward(input4) as DoublesTensor + assertEquals(Shape(1, 1, 3, 3), result4.shape) // (5 + 2*1 - 3) / 2 + 1 = 3 + } + + @Test + fun testConv2dWithNoBias() { + // Create a simple input tensor + val input = DoublesTensor(Shape(1, 1, 3, 3), DoubleArray(9) { 1.0 }) + + // Create a Conv2d layer with no bias + val conv = Conv2d( + inChannels = 1, + outChannels = 1, + kernelSize = 2, + stride = 1, + padding = 0, + useBias = false + ) + + // Apply convolution + val result = conv.forward(input) as DoublesTensor + + // Expected output shape: 1x1x2x2 + assertEquals(Shape(1, 1, 2, 2), result.shape) + + // Check that the output contains non-zero values + assertTrue(result.elements.any { it != 0.0 }, "Output should contain non-zero values") + } +} From e5537be6e06f5979f003c273ebcd60fb803ca65d Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Sun, 8 Jun 2025 23:17:13 +0200 Subject: [PATCH 08/19] Initial version of maxpool2d Related-To: #6 --- .../kotlin/sk/ai/net/nn/MaxPool2d.kt | 72 +++++++++++++++++++ .../kotlin/sk/ai/net/nn/MaxPool2dTest.kt | 44 ++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 core/src/commonMain/kotlin/sk/ai/net/nn/MaxPool2d.kt create mode 100644 core/src/commonTest/kotlin/sk/ai/net/nn/MaxPool2dTest.kt diff --git a/core/src/commonMain/kotlin/sk/ai/net/nn/MaxPool2d.kt b/core/src/commonMain/kotlin/sk/ai/net/nn/MaxPool2d.kt new file mode 100644 index 00000000..ca255dd4 --- /dev/null +++ b/core/src/commonMain/kotlin/sk/ai/net/nn/MaxPool2d.kt @@ -0,0 +1,72 @@ +package sk.ai.net.nn + +import sk.ai.net.Shape +import sk.ai.net.Tensor +import sk.ai.net.impl.DoublesTensor + +/** + * 2D max pooling layer. + * Works with tensors of shape (N, C, H, W) or (C, H, W). + */ +class MaxPool2d( + val kernelSize: Int, + val stride: Int = kernelSize, + override val name: String = "MaxPool2d" +) : Module() { + override val modules: List + get() = emptyList() + + override fun forward(input: Tensor): Tensor = maxPool2d(input) + + private fun maxPool2d(input: Tensor): Tensor { + val tensor = input as DoublesTensor + val shape = tensor.shape + require(shape.rank == 3 || shape.rank == 4) { + "MaxPool2d expected 3D or 4D input tensor, but got shape $shape" + } + val batchSize: Int + val channels: Int + val height: Int + val width: Int + if (shape.rank == 4) { + batchSize = shape[0] + channels = shape[1] + height = shape[2] + width = shape[3] + } else { + batchSize = 1 + channels = shape[0] + height = shape[1] + width = shape[2] + } + + val outH = (height - kernelSize) / stride + 1 + val outW = (width - kernelSize) / stride + 1 + val outElements = DoubleArray(batchSize * channels * outH * outW) + var idx = 0 + for (n in 0 until batchSize) { + for (c in 0 until channels) { + for (i in 0 until outH) { + for (j in 0 until outW) { + var maxVal = Double.NEGATIVE_INFINITY + for (ki in 0 until kernelSize) { + for (kj in 0 until kernelSize) { + val h = i * stride + ki + val w = j * stride + kj + val value = if (shape.rank == 4) { + tensor[n, c, h, w] + } else { + tensor[c, h, w] + } + if (value > maxVal) maxVal = value + } + } + outElements[idx++] = maxVal + } + } + } + } + val outShape = Shape(batchSize, channels, outH, outW) + return DoublesTensor(outShape, outElements) + } +} diff --git a/core/src/commonTest/kotlin/sk/ai/net/nn/MaxPool2dTest.kt b/core/src/commonTest/kotlin/sk/ai/net/nn/MaxPool2dTest.kt new file mode 100644 index 00000000..05136db5 --- /dev/null +++ b/core/src/commonTest/kotlin/sk/ai/net/nn/MaxPool2dTest.kt @@ -0,0 +1,44 @@ +package sk.ai.net.nn + +import sk.ai.net.Shape +import sk.ai.net.dsl.network +import sk.ai.net.impl.DoublesTensor +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class MaxPool2dTest { + @Test + fun max_pool2d_basic() { + val input = DoublesTensor( + Shape(1, 1, 4, 4), + doubleArrayOf( + 1.0, 2.0, 3.0, 4.0, + 5.0, 6.0, 7.0, 8.0, + 9.0, 10.0, 11.0, 12.0, + 13.0, 14.0, 15.0, 16.0 + ) + ) + val pool = MaxPool2d(kernelSize = 2, stride = 2) + val result = pool.forward(input) as DoublesTensor + assertEquals(Shape(1, 1, 2, 2), result.shape) + assertContentEquals(doubleArrayOf(6.0, 8.0, 14.0, 16.0), result.elements) + } + + @Test + fun dsl_support() { + val module = network { + input(1) + maxPool2d { + kernelSize = 2 + stride = 2 + } + } + val mlp = module as sk.ai.net.nn.topology.MLP + assertTrue(mlp.modules[1] is MaxPool2d) + val mp = mlp.modules[1] as MaxPool2d + assertEquals(2, mp.kernelSize) + assertEquals(2, mp.stride) + } +} From c24fac1bd8b1d89893cd6e46b0104ea813562224 Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Sun, 8 Jun 2025 23:17:43 +0200 Subject: [PATCH 09/19] Fix tests for flatten, maxpool, covn2d --- build.gradle.kts | 2 +- .../kotlin/sk/ai/net/Distribution.kt | 32 ++++ core/src/commonMain/kotlin/sk/ai/net/Shape.kt | 17 +++ .../src/commonMain/kotlin/sk/ai/net/Tensor.kt | 29 ++-- .../kotlin/sk/ai/net/TensorFactory.kt | 21 ++- .../commonMain/kotlin/sk/ai/net/core/Slice.kt | 15 ++ .../sk/ai/net/{impl => core}/TypedTensor.kt | 6 +- .../kotlin/sk/ai/net/dsl/NetworkBuilder.kt | 102 +++++++++++++ .../commonMain/kotlin/sk/ai/net/dsl/slice.kt | 144 ++++++++++++++++++ .../sk/ai/net/impl/DefaultTensorFactory.kt | 7 + .../kotlin/sk/ai/net/impl/DoublesTensor.kt | 46 ++++-- .../kotlin/sk/ai/net/impl/TensorFactories.kt | 18 ++- .../commonTest/kotlin/sk/ai/net/ShapeTest.kt | 40 ++++- .../kotlin/sk/ai/net/dsl/SlicesTest.kt | 42 +++++ .../kotlin/sk/ai/net/utils/assertions.kt | 30 ++++ gradle/libs.versions.toml | 2 +- 16 files changed, 520 insertions(+), 33 deletions(-) create mode 100644 core/src/commonMain/kotlin/sk/ai/net/Distribution.kt create mode 100644 core/src/commonMain/kotlin/sk/ai/net/core/Slice.kt rename core/src/commonMain/kotlin/sk/ai/net/{impl => core}/TypedTensor.kt (80%) create mode 100644 core/src/commonMain/kotlin/sk/ai/net/dsl/slice.kt create mode 100644 core/src/commonTest/kotlin/sk/ai/net/dsl/SlicesTest.kt create mode 100644 core/src/commonTest/kotlin/sk/ai/net/utils/assertions.kt diff --git a/build.gradle.kts b/build.gradle.kts index 083ab137..e5faf253 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,7 +10,7 @@ plugins { allprojects { group = "sk.ai.net" - version = "0.0.6" + version = "0.0.6.1" } moduleGraphConfig { diff --git a/core/src/commonMain/kotlin/sk/ai/net/Distribution.kt b/core/src/commonMain/kotlin/sk/ai/net/Distribution.kt new file mode 100644 index 00000000..faeb8ce5 --- /dev/null +++ b/core/src/commonMain/kotlin/sk/ai/net/Distribution.kt @@ -0,0 +1,32 @@ +package sk.ai.net + + +interface Distribution { + + /** + * Seed for the random number generator. + */ + val seed: Long + + /** + * Retrieves a new sample from the distribution. + * + * @return A T value. + */ + fun sample(): T + + + /** + * Retrieves the distribution's mean. + * + * @return The mean value. + */ + val mean: T + + /** + * Retrieves the distribution's standard deviation. + * + * @return The standard deviation. + */ + val deviation: T +} diff --git a/core/src/commonMain/kotlin/sk/ai/net/Shape.kt b/core/src/commonMain/kotlin/sk/ai/net/Shape.kt index 37333b14..0ea4e0fa 100644 --- a/core/src/commonMain/kotlin/sk/ai/net/Shape.kt +++ b/core/src/commonMain/kotlin/sk/ai/net/Shape.kt @@ -1,5 +1,6 @@ package sk.ai.net +import sk.ai.net.impl.assert import sk.ai.net.impl.zipFold class Shape(vararg dimensions: Int) { @@ -11,6 +12,22 @@ class Shape(vararg dimensions: Int) { val rank: Int get() = dimensions.size + internal fun index(indices: IntArray): Int { + assert( + { indices.size == dimensions.size }, + { "`indices.size` must be ${dimensions.size}: ${indices.size}" }) + return dimensions.zip(indices).fold(0) { a, x -> + assert( + { 0 <= x.second && x.second < x.first }, + { "Illegal index: indices = ${indices}, shape = $dimensions" }) + a * x.first + x.second + } + } + + operator fun get(vararg indices: Int): Int { + return dimensions[index(indices)] + } + override fun equals(other: Any?): Boolean { if (other !is Shape) { return false diff --git a/core/src/commonMain/kotlin/sk/ai/net/Tensor.kt b/core/src/commonMain/kotlin/sk/ai/net/Tensor.kt index 18d33608..aab123e6 100644 --- a/core/src/commonMain/kotlin/sk/ai/net/Tensor.kt +++ b/core/src/commonMain/kotlin/sk/ai/net/Tensor.kt @@ -50,7 +50,7 @@ interface Tensor { fun relu(): Tensor - fun softmax(i: Int): Tensor + fun softmax(dim: Int): Tensor fun softmax(): Tensor @@ -63,30 +63,35 @@ interface Tensor { fun cos(): Tensor fun tan(): Tensor - + fun asin(): Tensor fun acos(): Tensor - + fun atan(): Tensor - fun sinh():Tensor + fun sinh(): Tensor - fun cosh():Tensor + fun cosh(): Tensor - fun tanh():Tensor + fun tanh(): Tensor - fun exp():Tensor + fun exp(): Tensor - fun log():Tensor + fun log(): Tensor - fun sqrt():Tensor + fun sqrt(): Tensor - fun cbrt():Tensor + fun cbrt(): Tensor - fun sigmoid():Tensor + fun sigmoid(): Tensor - fun ln():Tensor + fun ln(): Tensor + + fun flatten(startDim: Int = 1, endDim: Int = -1): Tensor +} +fun Shape.toRanges(): Array { + return dimensions.map { 0 until it -1 }.toTypedArray() } diff --git a/core/src/commonMain/kotlin/sk/ai/net/TensorFactory.kt b/core/src/commonMain/kotlin/sk/ai/net/TensorFactory.kt index 9f891289..06224e15 100644 --- a/core/src/commonMain/kotlin/sk/ai/net/TensorFactory.kt +++ b/core/src/commonMain/kotlin/sk/ai/net/TensorFactory.kt @@ -1,5 +1,24 @@ package sk.ai.net +import sk.ai.net.impl.BuiltInDoubleDataDescriptor +import sk.ai.net.impl.DoublesTensor +import kotlin.random.Random + interface TensorFactory { fun createTensor(shape: Shape, dataDescriptor: DataDescriptor, elements: DoubleArray): Tensor -} \ No newline at end of file +} + +interface DataDescriptorFactory { + fun createDataDescriptor(): DataDescriptor +} + + +fun rand(shape: Shape, dataDescriptor: DataDescriptor = BuiltInDoubleDataDescriptor()): Tensor { + val random: Random = Random.Default + + return DoublesTensor(shape, DoubleArray(shape.volume) { random.nextFloat().toDouble() }) +} + +fun zeros(shape: Shape, dataDescriptor: DataDescriptor = BuiltInDoubleDataDescriptor()): Tensor { + return DoublesTensor(shape, DoubleArray(shape.volume)) +} diff --git a/core/src/commonMain/kotlin/sk/ai/net/core/Slice.kt b/core/src/commonMain/kotlin/sk/ai/net/core/Slice.kt new file mode 100644 index 00000000..cb82a91a --- /dev/null +++ b/core/src/commonMain/kotlin/sk/ai/net/core/Slice.kt @@ -0,0 +1,15 @@ +package sk.ai.net.core + +import sk.ai.net.Tensor + +data class Slice(val tensor: Tensor, val dimensionIndex: Int, val startIndex: Long, val endIndex: Long) { + fun toRange() = startIndex..endIndex +} + +fun Slice.start() = startIndex + +fun Slice.end() = endIndex + +fun Slice.all() = startIndex..tensor.shape.dimensions[dimensionIndex] + +fun Slice.range(): LongRange = startIndex..endIndex diff --git a/core/src/commonMain/kotlin/sk/ai/net/impl/TypedTensor.kt b/core/src/commonMain/kotlin/sk/ai/net/core/TypedTensor.kt similarity index 80% rename from core/src/commonMain/kotlin/sk/ai/net/impl/TypedTensor.kt rename to core/src/commonMain/kotlin/sk/ai/net/core/TypedTensor.kt index 9d2c6dcd..d8c3cd39 100644 --- a/core/src/commonMain/kotlin/sk/ai/net/impl/TypedTensor.kt +++ b/core/src/commonMain/kotlin/sk/ai/net/core/TypedTensor.kt @@ -1,4 +1,4 @@ -package sk.ai.net.impl +package sk.ai.net.core import sk.ai.net.Tensor @@ -11,7 +11,9 @@ interface TypedTensor : Tensor { operator fun get(vararg indices: Int): T operator fun get(vararg ranges: IntRange): TypedTensor -} + operator fun get(vararg ranges: Slice): Tensor + val allElements: List +} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/sk/ai/net/dsl/NetworkBuilder.kt b/core/src/commonMain/kotlin/sk/ai/net/dsl/NetworkBuilder.kt index 7bb0db14..7141616c 100644 --- a/core/src/commonMain/kotlin/sk/ai/net/dsl/NetworkBuilder.kt +++ b/core/src/commonMain/kotlin/sk/ai/net/dsl/NetworkBuilder.kt @@ -3,8 +3,11 @@ package sk.ai.net.dsl import sk.ai.net.nn.activations.ActivationsWrapperModule import sk.ai.net.Shape import sk.ai.net.Tensor +import sk.ai.net.nn.Flatten import sk.ai.net.nn.Input import sk.ai.net.nn.Linear +import sk.ai.net.nn.Conv2d +import sk.ai.net.nn.MaxPool2d import sk.ai.net.nn.Module import sk.ai.net.nn.topology.MLP @@ -25,7 +28,15 @@ interface NetworkDslItem interface NeuralNetworkDsl : NetworkDslItem { fun input(inputSize: Int, id: String = "") + fun flatten(id: String = "", content: FLATTEN.() -> Unit = {}) + + fun conv2d(id: String = "", content: CONV2D.() -> Unit = {}) + + fun maxPool2d(id: String = "", content: MAXPOOL2D.() -> Unit = {}) + fun dense(outputDimension: Int, id: String = "", content: DENSE.() -> Unit = {}) + + fun activation(id: String = "", activation: (Tensor) -> Tensor) } @NetworkDsl @@ -35,6 +46,27 @@ interface DENSE : NetworkDslItem { fun bias(initBlock: (Shape) -> Tensor) } +@NetworkDsl +interface FLATTEN : NetworkDslItem { + var startDim: Int + var endDim: Int +} + +@NetworkDsl +interface CONV2D : NetworkDslItem { + var outChannels: Int + var kernelSize: Int + var stride: Int + var padding: Int +} + +@NetworkDsl +interface MAXPOOL2D : NetworkDslItem { + var kernelSize: Int + var stride: Int +} + + private fun getDefaultName(id: String, s: String, size: Int): String { if (id.isNotEmpty()) return id return "$s-$size" @@ -69,6 +101,16 @@ fun createLinear( } } +class FlattenImpl( + override var startDim: Int = 1, + override var endDim: Int = -1, + private val id: String +) : FLATTEN { + fun create(): Module { + return Flatten(startDim, endDim, id) + } +} + class DenseImpl( private val inputDimension: Int, private val outputDimension: Int, private val id: String ) : DENSE { @@ -108,6 +150,36 @@ class DenseImpl( } } +class Conv2dImpl( + private val inChannels: Int, + override var outChannels: Int = 1, + override var kernelSize: Int = 3, + override var stride: Int = 1, + override var padding: Int = 0, + private val id: String +) : CONV2D { + fun create(): Module = Conv2d( + inChannels = inChannels, + outChannels = outChannels, + kernelSize = kernelSize, + stride = stride, + padding = padding, + name = id + ) +} + +class MaxPool2dImpl( + override var kernelSize: Int = 2, + override var stride: Int = 2, + private val id: String +) : MAXPOOL2D { + fun create(): Module = MaxPool2d( + kernelSize = kernelSize, + stride = stride, + name = id + ) +} + private class NeuralNetworkDslImpl : NeuralNetworkDsl { val modules = mutableListOf() @@ -119,6 +191,32 @@ private class NeuralNetworkDslImpl : NeuralNetworkDsl { modules.add(Input(Shape(inputSize), name = getDefaultName(id, "Input", modules.size))) } + override fun flatten(id: String, content: FLATTEN.() -> Unit) { + val impl = FlattenImpl( + id = getDefaultName(id, "flatten", modules.size) + ) + impl.content() + modules += impl.create() + } + + override fun conv2d(id: String, content: CONV2D.() -> Unit) { + val impl = Conv2dImpl( + inChannels = lastDimension, + id = getDefaultName(id, "conv2d", modules.size) + ) + impl.content() + lastDimension = impl.outChannels + modules += impl.create() + } + + override fun maxPool2d(id: String, content: MAXPOOL2D.() -> Unit) { + val impl = MaxPool2dImpl( + id = getDefaultName(id, "maxPool2d", modules.size) + ) + impl.content() + modules += impl.create() + } + override fun dense(outputDimension: Int, id: String, content: DENSE.() -> Unit) { val inputDimension = lastDimension lastDimension = outputDimension @@ -131,6 +229,10 @@ private class NeuralNetworkDslImpl : NeuralNetworkDsl { // dense layer consinst from linear module and activation function module (2 modules) modules += impl.create() } + + override fun activation(id: String, activation: (Tensor) -> Tensor) { + modules += ActivationsWrapperModule(activation, getDefaultName(id, "activation", modules.size)) + } } diff --git a/core/src/commonMain/kotlin/sk/ai/net/dsl/slice.kt b/core/src/commonMain/kotlin/sk/ai/net/dsl/slice.kt new file mode 100644 index 00000000..deef8b80 --- /dev/null +++ b/core/src/commonMain/kotlin/sk/ai/net/dsl/slice.kt @@ -0,0 +1,144 @@ +package sk.ai.net.dsl + +import sk.ai.net.Shape +import sk.ai.net.Tensor +import sk.ai.net.core.Slice +import sk.ai.net.impl.DoublesTensor +import sk.ai.net.core.TypedTensor +import kotlin.math.abs + + +/** +slices { +// from second to the last +slice { +from 2 to last +} +// all elemnts, equals to ":" in pandas +slice { +all +} +// from 0 to the second last element +slice { +to -2 +} +// from 0 to the second last element +slice { +from 2 to -2 +} +} + +slices { +start { +all() +} +end { +} +} + + */ + +class SliceBuilder(private val tensor: Tensor, private val dimensionIndex: Int) { + var startIndex: Long = 0 + var endIndex: Long = tensor.shape.dimensions[dimensionIndex].toLong() + + infix fun from(start: Int): FromBuilder { + this.startIndex = start.toLong() + return FromBuilder(this) + } + + infix fun up(end: Int): FromBuilder { + this.startIndex = 0 + if (end > 0) { + this.endIndex = end.toLong() + } else { + this.endIndex = tensor.shape.dimensions[dimensionIndex] - abs(end.toLong()) + } + return FromBuilder(this) + } + + fun all() { + this.startIndex = 0 + this.endIndex = tensor.shape.dimensions[dimensionIndex].toLong() + } + + fun first() { + this.startIndex = 0 + this.endIndex = 0 + } + + + fun last() { + this.startIndex = tensor.shape.dimensions[dimensionIndex].toLong() + this.endIndex = tensor.shape.dimensions[dimensionIndex].toLong() + } + + fun none() { + this.startIndex = -1 + this.endIndex = 0 + } + + + fun build() = Slice(tensor, dimensionIndex, startIndex, endIndex) + + inner class FromBuilder(private val sliceBuilder: SliceBuilder) { + infix fun to(end: Int) { + sliceBuilder.endIndex = if (end == -1) tensor.shape.dimensions[dimensionIndex].toLong() else end.toLong() + } + } +} + +class SlicesBuilder(private val tensor: Tensor) { + private val slices = mutableListOf() + + fun segment(init: SliceBuilder.() -> Unit) { + val dimensionIndex = slices.size + val builder = SliceBuilder(tensor, dimensionIndex) + builder.init() + slices.add(builder.build()) + } + + fun build() = slices +} + +fun slice(tensor: TypedTensor, init: SlicesBuilder.() -> Unit): TypedTensor { + val builder = SlicesBuilder(tensor) + builder.init() + val slices = builder.build() + return tensor.get(*slices.toTypedArray()) as TypedTensor +} + +fun printVarargs(vararg elements: Slice) { + for (element in elements) { + println(element) + } +} + +fun main() { + val tensor = DoublesTensor( + Shape(3, 3), + doubleArrayOf(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0) + ) + + val slicedTensor = slice(tensor) { + // from second to the last + segment { + all() + } + // all elements, equals to ":" + // from 0 to the second last element + segment { + from(1) + } + } + +// val s: Array = sliceList.toTypedArray() + // printVarargs(*s) + + println(tensor) + println(slicedTensor) + + //sliceList.forEach { println(tensor[it.toRange()]) } +} + + diff --git a/core/src/commonMain/kotlin/sk/ai/net/impl/DefaultTensorFactory.kt b/core/src/commonMain/kotlin/sk/ai/net/impl/DefaultTensorFactory.kt index b04e6372..a2d32fda 100644 --- a/core/src/commonMain/kotlin/sk/ai/net/impl/DefaultTensorFactory.kt +++ b/core/src/commonMain/kotlin/sk/ai/net/impl/DefaultTensorFactory.kt @@ -1,6 +1,7 @@ package sk.ai.net.impl import sk.ai.net.DataDescriptor +import sk.ai.net.DataDescriptorFactory import sk.ai.net.Shape import sk.ai.net.Tensor import sk.ai.net.TensorFactory @@ -13,4 +14,10 @@ class DefaultTensorFactory : TensorFactory { throw IllegalArgumentException("Unsupported data descriptor type: $dataDescriptor") } } +} + +class DefaultDataDescriptorFactory : DataDescriptorFactory { + override fun createDataDescriptor(): DataDescriptor { + return BuiltInDoubleDataDescriptor() + } } \ No newline at end of file diff --git a/core/src/commonMain/kotlin/sk/ai/net/impl/DoublesTensor.kt b/core/src/commonMain/kotlin/sk/ai/net/impl/DoublesTensor.kt index 588e7d5f..498f600f 100644 --- a/core/src/commonMain/kotlin/sk/ai/net/impl/DoublesTensor.kt +++ b/core/src/commonMain/kotlin/sk/ai/net/impl/DoublesTensor.kt @@ -3,9 +3,15 @@ package sk.ai.net.impl import sk.ai.net.DataDescriptor import sk.ai.net.Shape import sk.ai.net.Tensor +import sk.ai.net.core.Slice +import sk.ai.net.core.TypedTensor +import sk.ai.net.core.end +import sk.ai.net.core.start import kotlin.collections.map import kotlin.math.exp import kotlin.math.pow +import kotlin.random.Random +import kotlin.text.toInt data class DoublesTensor(override val shape: Shape, val elements: DoubleArray) : TypedTensor { constructor(shape: Shape, element: Double = 0.0) : this( @@ -36,6 +42,17 @@ data class DoublesTensor(override val shape: Shape, val elements: DoubleArray) : return elements[index(indices)] } + override operator fun get(vararg ranges: Slice): Tensor { + val intRanges = ranges.toList().map { s -> + IntRange(s.startIndex.toInt(), s.endIndex.toInt() - 1) + }.toTypedArray() + return this.get(*intRanges) + } + + override val allElements: List + get() = elements.toList() + + override operator fun get(vararg ranges: IntRange): TypedTensor { val size = ranges.size val shape = ranges.map { x -> x.last - x.first + 1 } @@ -180,15 +197,7 @@ data class DoublesTensor(override val shape: Shape, val elements: DoubleArray) : matrixToString() } - else -> { - val values = if (elements.size < 100) { - elements.contentToString() - } else { - elements.slice(0..100).toDoubleArray().contentToString() - } - - "Tensor($shape, $values)" // higher dimensions - } + else -> "Tensor(${shape}, ${elements.contentToString()})" // higher dimensions } } @@ -273,7 +282,7 @@ data class DoublesTensor(override val shape: Shape, val elements: DoubleArray) : return DoublesTensor(newShape, result) } - throw IllegalArgumentException("Unsupported tensor shapes for multiplication. $this $other") + throw IllegalArgumentException("Unsupported tensor shapes for multiplication.") } override fun t(): Tensor { @@ -369,6 +378,23 @@ data class DoublesTensor(override val shape: Shape, val elements: DoubleArray) : override fun ln(): Tensor = DoublesTensor(shape, elements.map { kotlin.math.ln(it) }.toDoubleArray()) + override fun flatten(startDim: Int, endDim: Int): Tensor { + val dims = shape.dimensions.toMutableList() + var s = if (startDim < 0) dims.size + startDim else startDim + var e = if (endDim < 0) dims.size + endDim else endDim + + // handle tensors without batch dimension by prepending 1 + while (dims.size <= e) { + dims.add(0, 1) + s += 1 + e += 1 + } + + val flatSize = dims.subList(s, e + 1).fold(1) { acc, v -> acc * v } + val newDims = dims.take(s) + flatSize + dims.drop(e + 1) + return DoublesTensor(Shape(*newDims.toIntArray()), elements.copyOf()) + } + fun computeStrides(dimensions: IntArray): IntArray { val strides = IntArray(dimensions.size) { 1 } for (i in dimensions.lastIndex - 1 downTo 0) { diff --git a/core/src/commonMain/kotlin/sk/ai/net/impl/TensorFactories.kt b/core/src/commonMain/kotlin/sk/ai/net/impl/TensorFactories.kt index 15ac75ab..9ed8e289 100644 --- a/core/src/commonMain/kotlin/sk/ai/net/impl/TensorFactories.kt +++ b/core/src/commonMain/kotlin/sk/ai/net/impl/TensorFactories.kt @@ -7,6 +7,22 @@ fun createTensor(values: DoubleArray): Tensor { return DoublesTensor(Shape(), values) } +fun createTensor(values: IntArray): Tensor { + return DoublesTensor(Shape(), values.map { it.toDouble() }.toDoubleArray()) +} + fun createTensor(shape: Shape, values: DoubleArray): Tensor { - return DoublesTensor(shape, values) + return DefaultTensorFactory().createTensor( + shape = shape, + dataDescriptor = DefaultDataDescriptorFactory().createDataDescriptor(), + elements = values + ) +} + +fun createTensor(shape: Shape, values: IntArray): Tensor { + return DefaultTensorFactory().createTensor( + shape = shape, + dataDescriptor = DefaultDataDescriptorFactory().createDataDescriptor(), + elements = values.map { it.toDouble() }.toDoubleArray() + ) } \ No newline at end of file diff --git a/core/src/commonTest/kotlin/sk/ai/net/ShapeTest.kt b/core/src/commonTest/kotlin/sk/ai/net/ShapeTest.kt index 9ac5f7fa..04307fac 100644 --- a/core/src/commonTest/kotlin/sk/ai/net/ShapeTest.kt +++ b/core/src/commonTest/kotlin/sk/ai/net/ShapeTest.kt @@ -2,13 +2,43 @@ package sk.ai.net import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertNotEquals +import kotlin.test.assertFalse +import kotlin.test.assertContentEquals class ShapeTest { @Test - fun `test scalar`() { - val shape = Shape(0) - assertNotEquals(shape, Shape(1, 2)) + fun volumeIsProductOfDimensions() { + val shape = Shape(2, 3, 4) + assertEquals(24, shape.volume) } -} \ No newline at end of file + + @Test + fun rankIsNumberOfDimensions() { + val shape = Shape(2, 3) + assertEquals(2, shape.rank) + } + + @Test + fun equalityChecksAllDimensions() { + val shape1 = Shape(2, 3) + val shape2 = Shape(2, 3) + val shape3 = Shape(3, 2) + assertEquals(shape1, shape2) + assertFalse(shape1 == shape3) + } + + @Test + fun constructorCopiesDimensions() { + val dims = intArrayOf(2, 3) + val shape = Shape(*dims) + dims[0] = 5 + assertContentEquals(intArrayOf(2, 3), shape.dimensions) + } + + @Test + fun toStringContainsSizeInformation() { + val shape = Shape(2, 3) + assertEquals("Shape: Dimensions = [2 x 3], Size (Volume) = 6", shape.toString()) + } +} diff --git a/core/src/commonTest/kotlin/sk/ai/net/dsl/SlicesTest.kt b/core/src/commonTest/kotlin/sk/ai/net/dsl/SlicesTest.kt new file mode 100644 index 00000000..f00b0863 --- /dev/null +++ b/core/src/commonTest/kotlin/sk/ai/net/dsl/SlicesTest.kt @@ -0,0 +1,42 @@ +package sk.ai.net.dsl + +import sk.ai.net.Shape +import sk.ai.net.core.TypedTensor +import sk.ai.net.impl.DoublesTensor +import sk.ai.net.impl.createTensor +import sk.ai.net.utils.assertTensorsSimilar +import kotlin.math.abs +import kotlin.test.Test + +class SlicesTest { + + @Test + fun `test slice Rank-1 slice`() { + + val tensor = DoublesTensor( + Shape(3, 3), + doubleArrayOf(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0) + ) + + val slicedTensor = slice(tensor) { + // from second to the last + segment { + all() + } + // all elements, equals to ":" + // from 0 to the second last element + segment { + from(1) + } + } + assertTensorsSimilar( + createTensor(Shape(3, 2), listOf(2, 3, 5, 6, 8, 9).toIntArray()) as TypedTensor, + slicedTensor, + message = "Sliced tensor is not as expected", + ::compareBy + ) + } + + fun compareBy(actual: Double, expected: Double): Boolean = + abs(actual - expected) < 0.0001 +} \ No newline at end of file diff --git a/core/src/commonTest/kotlin/sk/ai/net/utils/assertions.kt b/core/src/commonTest/kotlin/sk/ai/net/utils/assertions.kt new file mode 100644 index 00000000..57756932 --- /dev/null +++ b/core/src/commonTest/kotlin/sk/ai/net/utils/assertions.kt @@ -0,0 +1,30 @@ +package sk.ai.net.utils + +import sk.ai.net.core.TypedTensor +import kotlin.math.abs + +fun assertTensorsSimilar(expected: TypedTensor, actual: TypedTensor, message: String? = null, comparator:(T, T) -> Boolean) { + if (expected.shape != actual.shape) { + throw AssertionError("Shapes are different: expected ${expected.shape}, actual ${actual.shape}") + } + if (expected.dataDescriptor != actual.dataDescriptor) { + throw AssertionError("Data descriptors are different: expected ${expected.dataDescriptor}, actual ${actual.dataDescriptor}") + } + + val expectedRanges = expected.shape.dimensions + val actualRanges = actual.shape.dimensions + // write code comparing the contents of the tensors by iterating over the elements by ranges of dimensions + for (i in expectedRanges.indices) { + val expectedRange = expectedRanges[i] + val actualRange = actualRanges[i] + if (expectedRange != actualRange) { + throw AssertionError("Ranges are different: expected $expectedRange, actual $actualRange") + } + } + + expected.allElements.forEachIndexed { index, expectedElement: T -> + if (!comparator(expectedElement, actual.allElements[index])) { + throw AssertionError("Elements are different at index $index: expected $expectedElement, actual ${actual.allElements[index]}") + } + } + } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3aff7143..0b27575b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.10.0-alpha05" +agp = "8.10.0" kotlin = "2.1.21" kotlinx-coroutines = "1.10.2" android-minSdk = "24" From 1a586e86c96dbe5729ff90b51da54903d70b6e06 Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Sun, 8 Jun 2025 23:42:28 +0200 Subject: [PATCH 10/19] Add support for stages with DSL --- .../kotlin/sk/ai/net/dsl/NetworkBuilder.kt | 150 +++++++++++++++++- .../sk/ai/net/dsl/NetworkBuilderTest.kt | 59 +++++++ 2 files changed, 201 insertions(+), 8 deletions(-) create mode 100644 core/src/commonTest/kotlin/sk/ai/net/dsl/NetworkBuilderTest.kt diff --git a/core/src/commonMain/kotlin/sk/ai/net/dsl/NetworkBuilder.kt b/core/src/commonMain/kotlin/sk/ai/net/dsl/NetworkBuilder.kt index 7141616c..4511ab38 100644 --- a/core/src/commonMain/kotlin/sk/ai/net/dsl/NetworkBuilder.kt +++ b/core/src/commonMain/kotlin/sk/ai/net/dsl/NetworkBuilder.kt @@ -35,13 +35,20 @@ interface NeuralNetworkDsl : NetworkDslItem { fun maxPool2d(id: String = "", content: MAXPOOL2D.() -> Unit = {}) fun dense(outputDimension: Int, id: String = "", content: DENSE.() -> Unit = {}) + + fun dense(id: String = "", content: DENSE.() -> Unit = {}) fun activation(id: String = "", activation: (Tensor) -> Tensor) + + fun sequential(content: NeuralNetworkDsl.() -> Unit) + + fun stage(id: String, content: NeuralNetworkDsl.() -> Unit) } @NetworkDsl interface DENSE : NetworkDslItem { var activation: (Tensor) -> Tensor + var units: Int fun weights(initBlock: (Shape) -> Tensor) fun bias(initBlock: (Shape) -> Tensor) } @@ -112,18 +119,21 @@ class FlattenImpl( } class DenseImpl( - private val inputDimension: Int, private val outputDimension: Int, private val id: String + private val inputDimension: Int, private var _outputDimension: Int, private val id: String ) : DENSE { private var weightsValue: Tensor? = null private var biasValue: Tensor? = null private var _activation: (Tensor) -> Tensor = { tensor -> tensor } - fun create(): List { + // Expose the output dimension + val outputDimension: Int + get() = _outputDimension + fun create(): List { val linear = createLinear( inFeatures = inputDimension, - outFeatures = outputDimension, + outFeatures = _outputDimension, id = id, myInitWeights = weightsValue, myInitBias = biasValue @@ -141,12 +151,18 @@ class DenseImpl( _activation = value } + override var units: Int + get() = _outputDimension + set(value) { + _outputDimension = value + } + override fun weights(initBlock: (Shape) -> Tensor) { - weightsValue = initBlock(Shape(outputDimension, inputDimension)) + weightsValue = initBlock(Shape(_outputDimension, inputDimension)) } override fun bias(initBlock: (Shape) -> Tensor) { - biasValue = initBlock(Shape(outputDimension)) + biasValue = initBlock(Shape(_outputDimension)) } } @@ -180,12 +196,100 @@ class MaxPool2dImpl( ) } +// Stage implementation +class StageImpl(private val id: String) : NeuralNetworkDsl { + val modules = mutableListOf() + var lastDimension = 0 + var inputDimension = 0 + + fun create(): Module = MLP(*modules.toTypedArray(), name = id) + + override fun input(inputSize: Int, id: String) { + lastDimension = inputSize + modules.add(Input(Shape(inputSize), name = getDefaultName(id, "Input", modules.size))) + } + + override fun flatten(id: String, content: FLATTEN.() -> Unit) { + val impl = FlattenImpl( + id = getDefaultName(id, "flatten", modules.size) + ) + impl.content() + modules += impl.create() + } + + override fun conv2d(id: String, content: CONV2D.() -> Unit) { + val impl = Conv2dImpl( + inChannels = lastDimension, + id = getDefaultName(id, "conv2d", modules.size) + ) + impl.content() + lastDimension = impl.outChannels + modules += impl.create() + } + + override fun maxPool2d(id: String, content: MAXPOOL2D.() -> Unit) { + val impl = MaxPool2dImpl( + id = getDefaultName(id, "maxPool2d", modules.size) + ) + impl.content() + modules += impl.create() + } + + override fun dense(outputDimension: Int, id: String, content: DENSE.() -> Unit) { + val inputDimension = lastDimension + lastDimension = outputDimension + val impl = DenseImpl( + inputDimension = inputDimension, + _outputDimension = outputDimension, + id = getDefaultName(id, "linear", modules.size) + ) + impl.content() + // dense layer consists of linear module and activation function module (2 modules) + modules += impl.create() + } + + override fun dense(id: String, content: DENSE.() -> Unit) { + // This version of dense requires units to be specified in the content block + val impl = DenseImpl( + inputDimension = lastDimension, + _outputDimension = 0, // Will be set in content block via units property + id = getDefaultName(id, "linear", modules.size) + ) + impl.content() + // Update lastDimension based on the units set in the content block + lastDimension = impl.outputDimension + // dense layer consists of linear module and activation function module (2 modules) + modules += impl.create() + } + + override fun activation(id: String, activation: (Tensor) -> Tensor) { + modules += ActivationsWrapperModule(activation, getDefaultName(id, "activation", modules.size)) + } + + override fun sequential(content: NeuralNetworkDsl.() -> Unit) { + val sequentialImpl = NeuralNetworkDslImpl() + sequentialImpl.lastDimension = lastDimension + sequentialImpl.content() + lastDimension = sequentialImpl.lastDimension + modules += sequentialImpl.create() + } + + override fun stage(id: String, content: NeuralNetworkDsl.() -> Unit) { + val stageImpl = StageImpl(id) + stageImpl.lastDimension = lastDimension + stageImpl.content() + lastDimension = stageImpl.lastDimension + modules += stageImpl.create() + } +} + private class NeuralNetworkDslImpl : NeuralNetworkDsl { val modules = mutableListOf() var lastDimension = 0 fun create() = NetworkBuilder().add(*modules.toTypedArray()).build() + override fun input(inputSize: Int, id: String) { lastDimension = inputSize modules.add(Input(Shape(inputSize), name = getDefaultName(id, "Input", modules.size))) @@ -222,17 +326,47 @@ private class NeuralNetworkDslImpl : NeuralNetworkDsl { lastDimension = outputDimension val impl = DenseImpl( inputDimension = inputDimension, - outputDimension = outputDimension, + _outputDimension = outputDimension, + id = getDefaultName(id, "linear", modules.size) + ) + impl.content() + // dense layer consists of linear module and activation function module (2 modules) + modules += impl.create() + } + + override fun dense(id: String, content: DENSE.() -> Unit) { + // This version of dense requires units to be specified in the content block + val impl = DenseImpl( + inputDimension = lastDimension, + _outputDimension = 0, // Will be set in content block via units property id = getDefaultName(id, "linear", modules.size) ) impl.content() - // dense layer consinst from linear module and activation function module (2 modules) + // Update lastDimension based on the units set in the content block + lastDimension = impl.outputDimension + // dense layer consists of linear module and activation function module (2 modules) modules += impl.create() } override fun activation(id: String, activation: (Tensor) -> Tensor) { modules += ActivationsWrapperModule(activation, getDefaultName(id, "activation", modules.size)) } + + override fun sequential(content: NeuralNetworkDsl.() -> Unit) { + val sequentialImpl = NeuralNetworkDslImpl() + sequentialImpl.lastDimension = lastDimension + sequentialImpl.content() + lastDimension = sequentialImpl.lastDimension + modules += sequentialImpl.create() + } + + override fun stage(id: String, content: NeuralNetworkDsl.() -> Unit) { + val stageImpl = StageImpl(id) + stageImpl.lastDimension = lastDimension + stageImpl.content() + lastDimension = stageImpl.lastDimension + modules += stageImpl.create() + } } @@ -246,4 +380,4 @@ class NetworkBuilder { } fun build(): Module = MLP(*modules.toTypedArray()) -} +} \ No newline at end of file diff --git a/core/src/commonTest/kotlin/sk/ai/net/dsl/NetworkBuilderTest.kt b/core/src/commonTest/kotlin/sk/ai/net/dsl/NetworkBuilderTest.kt new file mode 100644 index 00000000..45cd0eb2 --- /dev/null +++ b/core/src/commonTest/kotlin/sk/ai/net/dsl/NetworkBuilderTest.kt @@ -0,0 +1,59 @@ +package sk.ai.net.dsl + +import sk.ai.net.nn.activations.ReLU +import sk.ai.net.nn.activations.Softmax +import kotlin.test.Test +import kotlin.test.assertNotNull + +class NetworkBuilderTest { + @Test + fun testNetworkWithStages() { + val mnistNetwork = network { + sequential { + stage("conv1") { + conv2d { + outChannels = 16 + kernelSize = 5 + stride = 1 + padding = 2 + } + activation("relu", ReLU()::forward) + maxPool2d { + kernelSize = 2 + stride = 2 + } + } + stage("conv2") { + conv2d { + outChannels = 32 + kernelSize = 5 + stride = 1 + padding = 2 + } + activation("relu", ReLU()::forward) + maxPool2d { + kernelSize = 2 + stride = 2 + } + } + stage("flatten") { + flatten() + } + stage("dense") { + dense { + units = 128 + } + activation("relu", ReLU()::forward) + } + stage("output") { + dense { + units = 10 + } + activation("softmax", Softmax(1)::forward) + } + } + } + + assertNotNull(mnistNetwork) + } +} From ec5434f8767f4e0c9bb28dc324c7babf1741e34f Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Sun, 8 Jun 2025 23:44:34 +0200 Subject: [PATCH 11/19] Fix unit test for conv2d Related-To: #4 --- .../commonMain/kotlin/sk/ai/net/nn/Conv2d.kt | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/core/src/commonMain/kotlin/sk/ai/net/nn/Conv2d.kt b/core/src/commonMain/kotlin/sk/ai/net/nn/Conv2d.kt index dfca5c9d..ddfd5a91 100644 --- a/core/src/commonMain/kotlin/sk/ai/net/nn/Conv2d.kt +++ b/core/src/commonMain/kotlin/sk/ai/net/nn/Conv2d.kt @@ -27,19 +27,27 @@ class Conv2d( init { // Initialize weights and bias val fanIn = inChannels * kernelSize * kernelSize - val bound = 1f / sqrt(fanIn.toDouble()).toFloat() // 1/sqrt(fanIn) + val bound = 1.0 / sqrt(fanIn.toDouble()) // 1/sqrt(fanIn) + // Weight: uniform in [-bound, bound] - weight = (((rand( - Shape( - outChannels, - inChannels, - kernelSize, - kernelSize - ) - ) as DoublesTensor) * (2f * bound).toDouble()) as DoublesTensor) - bound.toDouble() + // Create a tensor with random values in [0,1] + val weightShape = Shape(outChannels, inChannels, kernelSize, kernelSize) + val randomWeight = rand(weightShape) as DoublesTensor + + // Scale to [0, 2*bound] + val scaledWeight = randomWeight.times(2.0 * bound) + + // Shift to [-bound, bound] + val boundTensor = DoublesTensor(weightShape, DoubleArray(weightShape.volume) { bound }) + weight = scaledWeight.minus(boundTensor) + // Bias: uniform in [-bound, bound] if enabled bias = if (useBias) { - ((rand(Shape(outChannels)) as DoublesTensor) * (2f * bound).toDouble()) - bound.toDouble() + val biasShape = Shape(outChannels) + val randomBias = rand(biasShape) as DoublesTensor + val scaledBias = randomBias.times(2.0 * bound) + val boundBiasTensor = DoublesTensor(biasShape, DoubleArray(biasShape.volume) { bound }) + scaledBias.minus(boundBiasTensor) } else { null } @@ -58,15 +66,15 @@ class Conv2d( val inW: Int if (shape.rank == 4) { batchSize = shape.dimensions[0] - inC = shape[1] - inH = shape[2] - inW = shape[3] + inC = shape.dimensions[1] + inH = shape.dimensions[2] + inW = shape.dimensions[3] } else { // if 3D (C, H, W), treat as batch of size 1 batchSize = 1 - inC = shape[0] - inH = shape[1] - inW = shape[2] + inC = shape.dimensions[0] + inH = shape.dimensions[1] + inW = shape.dimensions[2] } require(inC == inChannels) { "Conv2d expected input channel count $inChannels, but got $inC." From f0bf1d6bcc9fffbb94fd8aa62bac12ab0fa7e1be Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Sun, 8 Jun 2025 23:50:12 +0200 Subject: [PATCH 12/19] Add docs for conv2d Related-To: #4 --- docs/conv2d.md | 302 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 302 insertions(+) create mode 100644 docs/conv2d.md diff --git a/docs/conv2d.md b/docs/conv2d.md new file mode 100644 index 00000000..4a74b066 --- /dev/null +++ b/docs/conv2d.md @@ -0,0 +1,302 @@ +# Conv2d: 2D Convolution Layer + +## Introduction + +The `Conv2d` class implements a 2D convolution layer for neural networks. Convolution is a fundamental operation in deep learning, especially for processing grid-like data such as images. The 2D convolution operation slides a kernel (or filter) over the input data, computing the element-wise product and sum at each position. + +In the context of neural networks, convolution layers are used to automatically and adaptively learn spatial hierarchies of features from input data. For image processing, early convolution layers might detect simple features like edges, while deeper layers can recognize more complex patterns like textures or even entire objects. + +## Class Parameters + +The `Conv2d` class in the SK-AI-Net library has the following parameters: + +```kotlin +class Conv2d( + val inChannels: Int, + val outChannels: Int, + val kernelSize: Int, + val stride: Int = 1, + val padding: Int = 0, + useBias: Boolean = true, + name: String = "Conv2d" +) : Module() +``` + +Let's explore each parameter in detail: + +### inChannels + +The number of input channels (or feature maps). For RGB images, this would typically be 3 (red, green, blue). For grayscale images, this would be 1. For layers deeper in the network, this corresponds to the number of feature maps produced by the previous layer. + +### outChannels + +The number of output channels (or feature maps) that the convolution layer will produce. This determines how many different filters will be applied to the input. Each filter learns to detect different features in the input. + +### kernelSize + +The size of the convolution kernel (or filter). This is a square kernel with dimensions `kernelSize × kernelSize`. Common values are 3×3, 5×5, or 7×7. Larger kernels can capture more spatial context but require more computation and may lead to overfitting. + +### stride + +The stride parameter controls how the kernel moves across the input. A stride of 1 means the kernel moves one pixel at a time, while a stride of 2 means it skips every other pixel. Larger strides result in smaller output dimensions and can be used for downsampling. + +### padding + +Padding adds extra pixels around the input data before applying the convolution. This is useful for preserving the spatial dimensions of the input. Without padding, the output dimensions would be smaller than the input. Common padding strategies include: + +- `padding = 0` (no padding): Output size will be smaller than input +- `padding = kernelSize / 2` (same padding): Output size will be the same as input (for stride=1) + +### useBias + +A boolean flag indicating whether to include a bias term in the convolution. When `true`, a learnable bias is added to each output channel after the convolution operation. + +### name + +An optional name for the layer, useful for debugging and model visualization. + +## Understanding Padding + +Padding is crucial for controlling the spatial dimensions of the output. Without padding, each convolution operation reduces the size of the feature map, which can be problematic for deep networks. + +### Effects of Padding + +- **No padding (padding = 0)**: The output dimensions will be smaller than the input. Specifically, if the input has spatial dimensions H×W, the output will have dimensions (H - kernelSize + 1)×(W - kernelSize + 1) (for stride=1). +- **Same padding (padding = kernelSize / 2)**: The output dimensions will be the same as the input (for stride=1). This is often desirable to maintain spatial information. +- **Valid padding (padding < kernelSize / 2)**: The output dimensions will be smaller than the input, but some padding is still applied. + +### Example + +For an input image of size 28×28 with a 3×3 kernel: + +- With no padding (padding = 0), the output size will be 26×26 +- With padding = 1, the output size will be 28×28 (same as input) + +## Understanding Strides + +The stride parameter determines how the kernel moves across the input data. It affects both the computational efficiency and the output dimensions. + +### Effects of Stride + +- **stride = 1**: The kernel moves one pixel at a time, resulting in the maximum possible output size. +- **stride > 1**: The kernel skips pixels, resulting in a smaller output size. This can be used for downsampling or reducing computational requirements. + +### Output Size Calculation + +The output size can be calculated using the formula: +``` +outputSize = (inputSize + 2 * padding - kernelSize) / stride + 1 +``` + +### Example + +For an input image of size 28×28 with a 3×3 kernel: + +- With padding = 1 and stride = 1, the output size will be 28×28 +- With padding = 1 and stride = 2, the output size will be 14×14 (downsampled by a factor of 2) + +## Understanding Filters/Kernels + +Filters (or kernels) are the learnable parameters in a convolution layer. Each filter is responsible for detecting specific features in the input data. + +### Role of Filters + +- Each filter is a small matrix (e.g., 3×3) that is convolved with the input +- The values in the filter are learned during training +- Different filters learn to detect different features (e.g., edges, textures, patterns) +- The number of filters determines the number of output channels + +### Filter Dimensions + +For a `Conv2d` layer with `inChannels` input channels and `outChannels` output channels, the total number of parameters in the filters is: +``` +parameters = outChannels * inChannels * kernelSize * kernelSize +``` + +If `useBias` is true, there are an additional `outChannels` bias parameters. + +## Sample Use Cases + +### Basic Convolution + +```kotlin +// Create a simple 1x1x3x3 input tensor (1 batch, 1 channel, 3x3 spatial dimensions) +val input = DoublesTensor( + Shape(1, 1, 3, 3), + doubleArrayOf( + 1.0, 2.0, 3.0, + 4.0, 5.0, 6.0, + 7.0, 8.0, 9.0 + ) +) + +// Create a Conv2d layer with 1 input channel, 1 output channel, 2x2 kernel, no stride, no padding +val conv = Conv2d( + inChannels = 1, + outChannels = 1, + kernelSize = 2, + stride = 1, + padding = 0, + useBias = true +) + +// Apply convolution +val result = conv.forward(input) as DoublesTensor + +// Expected output shape: 1x1x2x2 (1 batch, 1 channel, 2x2 spatial dimensions) +println(result.shape) // Shape(1, 1, 2, 2) +``` + +### Convolution with Padding + +```kotlin +// Create a simple 1x1x3x3 input tensor +val input = DoublesTensor( + Shape(1, 1, 3, 3), + doubleArrayOf( + 1.0, 2.0, 3.0, + 4.0, 5.0, 6.0, + 7.0, 8.0, 9.0 + ) +) + +// Create a Conv2d layer with padding=1 +val conv = Conv2d( + inChannels = 1, + outChannels = 1, + kernelSize = 3, + stride = 1, + padding = 1, + useBias = true +) + +// Apply convolution +val result = conv.forward(input) as DoublesTensor + +// Expected output shape: 1x1x3x3 (padding preserves spatial dimensions) +println(result.shape) // Shape(1, 1, 3, 3) +``` + +### Convolution with Stride + +```kotlin +// Create a simple 1x1x4x4 input tensor +val input = DoublesTensor( + Shape(1, 1, 4, 4), + doubleArrayOf( + 1.0, 2.0, 3.0, 4.0, + 5.0, 6.0, 7.0, 8.0, + 9.0, 10.0, 11.0, 12.0, + 13.0, 14.0, 15.0, 16.0 + ) +) + +// Create a Conv2d layer with stride=2 +val conv = Conv2d( + inChannels = 1, + outChannels = 1, + kernelSize = 2, + stride = 2, + padding = 0, + useBias = true +) + +// Apply convolution +val result = conv.forward(input) as DoublesTensor + +// Expected output shape: 1x1x2x2 (stride=2 reduces spatial dimensions by half) +println(result.shape) // Shape(1, 1, 2, 2) +``` + +### Multiple Input and Output Channels + +```kotlin +// Create a 1x2x3x3 input tensor (1 batch, 2 channels, 3x3 spatial dimensions) +val input = DoublesTensor( + Shape(1, 2, 3, 3), + doubleArrayOf( + // Channel 1 + 1.0, 2.0, 3.0, + 4.0, 5.0, 6.0, + 7.0, 8.0, 9.0, + // Channel 2 + 9.0, 8.0, 7.0, + 6.0, 5.0, 4.0, + 3.0, 2.0, 1.0 + ) +) + +// Create a Conv2d layer with 2 input channels, 3 output channels +val conv = Conv2d( + inChannels = 2, + outChannels = 3, + kernelSize = 2, + stride = 1, + padding = 0, + useBias = true +) + +// Apply convolution +val result = conv.forward(input) as DoublesTensor + +// Expected output shape: 1x3x2x2 (1 batch, 3 output channels, 2x2 spatial dimensions) +println(result.shape) // Shape(1, 3, 2, 2) +``` + +### Using Conv2d in a Network + +```kotlin +// Create a simple CNN for image classification +val network = network { + // Input layer for 1-channel 28x28 images (e.g., MNIST) + input(1) + + // First convolutional layer + conv2d { + outChannels = 16 + kernelSize = 3 + stride = 1 + padding = 1 + } + + // Apply ReLU activation + activation { it.relu() } + + // Max pooling layer + maxPool2d { + kernelSize = 2 + stride = 2 + } + + // Second convolutional layer + conv2d { + outChannels = 32 + kernelSize = 3 + stride = 1 + padding = 1 + } + + // Apply ReLU activation + activation { it.relu() } + + // Max pooling layer + maxPool2d { + kernelSize = 2 + stride = 2 + } + + // Flatten the output for the fully connected layer + flatten() + + // Fully connected layer with 10 output units (e.g., for 10 digit classes) + dense(10) +} + +// Use the network for inference +val input = DoublesTensor(Shape(1, 1, 28, 28), /* image data */) +val output = network.forward(input) +``` + +## Conclusion + +The `Conv2d` class is a powerful tool for building convolutional neural networks. By understanding the parameters and their effects, you can design effective architectures for various computer vision tasks. Experiment with different kernel sizes, strides, and padding values to find the configuration that works best for your specific application. \ No newline at end of file From e9dd4a610639562da1507df657b307f5ea89f58e Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Mon, 9 Jun 2025 00:13:00 +0200 Subject: [PATCH 13/19] Add docs for MaxPool2d Related-To: #6 --- .../kotlin/sk/ai/net/nn/MaxPool2d.kt | 18 +- docs/maxpool2d.md | 214 ++++++++++++++++++ 2 files changed, 223 insertions(+), 9 deletions(-) create mode 100644 docs/maxpool2d.md diff --git a/core/src/commonMain/kotlin/sk/ai/net/nn/MaxPool2d.kt b/core/src/commonMain/kotlin/sk/ai/net/nn/MaxPool2d.kt index ca255dd4..b3edc3bb 100644 --- a/core/src/commonMain/kotlin/sk/ai/net/nn/MaxPool2d.kt +++ b/core/src/commonMain/kotlin/sk/ai/net/nn/MaxPool2d.kt @@ -29,15 +29,15 @@ class MaxPool2d( val height: Int val width: Int if (shape.rank == 4) { - batchSize = shape[0] - channels = shape[1] - height = shape[2] - width = shape[3] + batchSize = shape.dimensions[0] + channels = shape.dimensions[1] + height = shape.dimensions[2] + width = shape.dimensions[3] } else { batchSize = 1 - channels = shape[0] - height = shape[1] - width = shape[2] + channels = shape.dimensions[0] + height = shape.dimensions[1] + width = shape.dimensions[2] } val outH = (height - kernelSize) / stride + 1 @@ -54,9 +54,9 @@ class MaxPool2d( val h = i * stride + ki val w = j * stride + kj val value = if (shape.rank == 4) { - tensor[n, c, h, w] + tensor.elements[n * channels * height * width + c * height * width + h * width + w] } else { - tensor[c, h, w] + tensor.elements[c * height * width + h * width + w] } if (value > maxVal) maxVal = value } diff --git a/docs/maxpool2d.md b/docs/maxpool2d.md new file mode 100644 index 00000000..f85fea8f --- /dev/null +++ b/docs/maxpool2d.md @@ -0,0 +1,214 @@ +# MaxPool2d: 2D Max Pooling Layer + +## Introduction + +The `MaxPool2d` class implements a 2D max pooling layer for neural networks. Max pooling is a downsampling operation that reduces the spatial dimensions of the input data by taking the maximum value in each pooling window. This operation is commonly used in convolutional neural networks (CNNs) to reduce the computational load, extract dominant features, and provide a form of translation invariance. + +In the context of neural networks, max pooling layers are typically inserted between successive convolutional layers to progressively reduce the spatial size of the representation, reduce the number of parameters and computation in the network, and control overfitting. + +## Class Parameters + +The `MaxPool2d` class in the SK-AI-Net library has the following parameters: + +```kotlin +class MaxPool2d( + val kernelSize: Int, + val stride: Int = kernelSize, + override val name: String = "MaxPool2d" +) : Module() +``` + +Let's explore each parameter in detail: + +### kernelSize + +The size of the pooling window. This is a square window with dimensions `kernelSize × kernelSize`. Common values are 2×2 or 3×3. The pooling operation takes the maximum value from each window and discards the rest of the information. + +### stride + +The stride parameter controls how the pooling window moves across the input. A stride of 1 means the window moves one pixel at a time, while a stride of 2 means it skips every other pixel. By default, the stride is set to the same value as the kernel size, which is a common practice for pooling layers. + +### name + +An optional name for the layer, useful for debugging and model visualization. + +## Understanding Max Pooling + +Max pooling is a form of non-linear downsampling. The operation partitions the input into a set of non-overlapping rectangles and, for each such sub-region, outputs the maximum value. + +### Benefits of Max Pooling + +1. **Dimensionality Reduction**: Reduces the spatial dimensions of the feature maps, leading to fewer parameters and computations in the network. +2. **Translation Invariance**: Provides a form of translation invariance, making the network more robust to small shifts in the input. +3. **Feature Extraction**: Helps in extracting dominant features by discarding less important information. +4. **Overfitting Control**: Reduces the risk of overfitting by providing a form of regularization. + +### Output Size Calculation + +The output size can be calculated using the formula: +``` +outputSize = (inputSize - kernelSize) / stride + 1 +``` + +### Example + +For an input feature map of size 4×4 with a 2×2 kernel and stride of 2: +- The output size will be (4 - 2) / 2 + 1 = 2, resulting in a 2×2 output feature map. +- Each value in the output is the maximum of a 2×2 region in the input. + +## Sample Use Cases + +### Basic Max Pooling + +```kotlin +// Create a simple 1x1x4x4 input tensor (1 batch, 1 channel, 4x4 spatial dimensions) +val input = DoublesTensor( + Shape(1, 1, 4, 4), + doubleArrayOf( + 1.0, 2.0, 3.0, 4.0, + 5.0, 6.0, 7.0, 8.0, + 9.0, 10.0, 11.0, 12.0, + 13.0, 14.0, 15.0, 16.0 + ) +) + +// Create a MaxPool2d layer with 2x2 kernel and stride=2 +val pool = MaxPool2d( + kernelSize = 2, + stride = 2 +) + +// Apply max pooling +val result = pool.forward(input) as DoublesTensor + +// Expected output shape: 1x1x2x2 (1 batch, 1 channel, 2x2 spatial dimensions) +println(result.shape) // Shape(1, 1, 2, 2) + +// Expected output values: the maximum value in each 2x2 window +// [6.0, 8.0, 14.0, 16.0] +println(result.elements.toList()) +``` + +### Max Pooling with Different Stride + +```kotlin +// Create a simple 1x1x5x5 input tensor +val input = DoublesTensor( + Shape(1, 1, 5, 5), + doubleArrayOf( + 1.0, 2.0, 3.0, 4.0, 5.0, + 6.0, 7.0, 8.0, 9.0, 10.0, + 11.0, 12.0, 13.0, 14.0, 15.0, + 16.0, 17.0, 18.0, 19.0, 20.0, + 21.0, 22.0, 23.0, 24.0, 25.0 + ) +) + +// Create a MaxPool2d layer with 3x3 kernel and stride=1 +val pool = MaxPool2d( + kernelSize = 3, + stride = 1 +) + +// Apply max pooling +val result = pool.forward(input) as DoublesTensor + +// Expected output shape: 1x1x3x3 (1 batch, 1 channel, 3x3 spatial dimensions) +println(result.shape) // Shape(1, 1, 3, 3) +``` + +### Max Pooling with Multiple Channels + +```kotlin +// Create a 1x3x4x4 input tensor (1 batch, 3 channels, 4x4 spatial dimensions) +val input = DoublesTensor( + Shape(1, 3, 4, 4), + doubleArrayOf( + // Channel 1 + 1.0, 2.0, 3.0, 4.0, + 5.0, 6.0, 7.0, 8.0, + 9.0, 10.0, 11.0, 12.0, + 13.0, 14.0, 15.0, 16.0, + // Channel 2 + 17.0, 18.0, 19.0, 20.0, + 21.0, 22.0, 23.0, 24.0, + 25.0, 26.0, 27.0, 28.0, + 29.0, 30.0, 31.0, 32.0, + // Channel 3 + 33.0, 34.0, 35.0, 36.0, + 37.0, 38.0, 39.0, 40.0, + 41.0, 42.0, 43.0, 44.0, + 45.0, 46.0, 47.0, 48.0 + ) +) + +// Create a MaxPool2d layer with 2x2 kernel and stride=2 +val pool = MaxPool2d( + kernelSize = 2, + stride = 2 +) + +// Apply max pooling +val result = pool.forward(input) as DoublesTensor + +// Expected output shape: 1x3x2x2 (1 batch, 3 channels, 2x2 spatial dimensions) +println(result.shape) // Shape(1, 3, 2, 2) +``` + +### Using MaxPool2d in a Network + +```kotlin +// Create a simple CNN for image classification +val network = network { + // Input layer for 1-channel 28x28 images (e.g., MNIST) + input(1) + + // First convolutional layer + conv2d { + outChannels = 16 + kernelSize = 3 + stride = 1 + padding = 1 + } + + // Apply ReLU activation + activation { it.relu() } + + // Max pooling layer + maxPool2d { + kernelSize = 2 + stride = 2 + } + + // Second convolutional layer + conv2d { + outChannels = 32 + kernelSize = 3 + stride = 1 + padding = 1 + } + + // Apply ReLU activation + activation { it.relu() } + + // Max pooling layer + maxPool2d { + kernelSize = 2 + stride = 2 + } + + // Flatten the output for the fully connected layer + flatten() + + // Fully connected layer with 10 output units (e.g., for 10 digit classes) + dense(10) +} + +// Use the network for inference +val input = DoublesTensor(Shape(1, 1, 28, 28), /* image data */) +val output = network.forward(input) +``` + +## Conclusion + +The `MaxPool2d` class is an essential component for building convolutional neural networks. By reducing spatial dimensions and extracting dominant features, max pooling helps create more efficient and effective neural network architectures. When designing your network, consider the appropriate kernel size and stride values for your specific application to balance between feature extraction and information preservation. \ No newline at end of file From a7b9705d4b082b5dd8b564e0c10404a3b1f07a27 Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Mon, 9 Jun 2025 00:31:04 +0200 Subject: [PATCH 14/19] Add note of diferiantion with conv2d Related-To: #4 --- docs/conv2d.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/docs/conv2d.md b/docs/conv2d.md index 4a74b066..c5332f46 100644 --- a/docs/conv2d.md +++ b/docs/conv2d.md @@ -6,6 +6,56 @@ The `Conv2d` class implements a 2D convolution layer for neural networks. Convol In the context of neural networks, convolution layers are used to automatically and adaptively learn spatial hierarchies of features from input data. For image processing, early convolution layers might detect simple features like edges, while deeper layers can recognize more complex patterns like textures or even entire objects. +### Why Conv2D is Differentiable +A Conv2D layer is differentiable because its operations are mathematically well-defined and have computable derivatives: + + * Convolution: For an input tensor $ X $ (e.g., shape (1, 28, 28) for MNIST), a kernel $ W $ (e.g., 5x5), and bias $ b $, the output at position $(i, j)$ for output channel $ k $ is: +$$Y_k[i, j] = \sum_{m,n,c} X[c, i+m, j+n] \cdot W_k[c, m, n] + b_k$$ +where $ m, n $ iterate over the kernel size, and $ c $ iterates over input channels. + +This is a linear operation (sum of products plus a constant), which is differentiable. +Partial derivatives with respect to weights ($ \frac{\partial Y}{\partial W} $) and input ($ \frac{\partial Y}{\partial X} $) are straightforward: + +$ \frac{\partial Y_k[i, j]}{\partial W_k[c, m, n]} = X[c, i+m, j+n] $ +$ \frac{\partial Y_k[i, j]}{\partial X[c, i+m, j+n]} = W_k[c, m, n] $ +$ \frac{\partial Y_k[i, j]}{\partial b_k} = 1 $ + + + + +Stride and Padding: Stride controls the step size of the kernel, and padding adds zeros around the input. Both are fixed operations that don’t affect differentiability, as they only modify indexing. +Output: The output $ Y $ is passed to the next layer (e.g., ReLU in your CNN), which is also differentiable. + +3. Role in Learning (Backpropagation) + The Conv2D layer supports learning through backpropagation, where gradients of the loss function $ L $ with respect to the weights and biases are computed and used to update parameters: + +Forward Pass: + +Input (e.g., MNIST image) passes through the Conv2D layer, producing feature maps (e.g., 16 channels of 28x28 for conv1, due to padding=2). +These feature maps are transformed by subsequent layers (ReLU, MaxPool2D, etc.) and eventually produce a prediction (e.g., probabilities for 10 digit classes). +The loss $ L $ (e.g., cross-entropy) compares the prediction to the true label. + + +Backward Pass: + +The gradient of the loss with respect to the output of Conv2D ($ \frac{\partial L}{\partial Y} $) is received from the next layer (e.g., ReLU). +Gradients for weights and biases are computed: + +Weight gradient: $ \frac{\partial L}{\partial W_k[c, m, n]} = \sum_{i,j} \frac{\partial L}{\partial Y_k[i, j]} \cdot X[c, i+m, j+n] $ +Bias gradient: $ \frac{\partial L}{\partial b_k} = \sum_{i,j} \frac{\partial L}{\partial Y_k[i, j]} $ +Input gradient (for backprop to earlier layers): $ \frac{\partial L}{\partial X[c, i+m, j+n]} = \sum_k \frac{\partial L}{\partial Y_k[i, j]} \cdot W_k[c, m, n] $ + + +These gradients are computed efficiently using cross-correlation (a convolution-like operation). + + +Parameter Update: + +An optimizer (e.g., SGD, Adam) updates the weights and biases: +$$W \gets W - \eta \cdot \frac{\partial L}{\partial W}, \quad b \gets b - \eta \cdot \frac{\partial L}{\partial b}$$ +where $ \eta $ is the learning rate. +This adjusts the kernels to better detect features (e.g., edges, shapes in MNIST digits) that minimize the loss. + ## Class Parameters The `Conv2d` class in the SK-AI-Net library has the following parameters: From 3e76da110f90034f43cd8e67b4e5c3887b7f4402 Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Sun, 15 Jun 2025 18:01:20 +0200 Subject: [PATCH 15/19] fix compiler warning and remove duplicated JVM code --- io/build.gradle.kts | 12 ++ model-zoo/build.gradle.kts | 15 +-- .../sk/ai/net/io/data/mnist/MNISTLoader.kt | 112 ------------------ 3 files changed, 20 insertions(+), 119 deletions(-) delete mode 100644 model-zoo/src/jvmMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoader.kt diff --git a/io/build.gradle.kts b/io/build.gradle.kts index 6bc3befa..01c82ff8 100644 --- a/io/build.gradle.kts +++ b/io/build.gradle.kts @@ -1,4 +1,7 @@ +@file:OptIn(ExperimentalWasmDsl::class) + import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { @@ -10,6 +13,15 @@ plugins { } kotlin { + + targets.configureEach { + compilations.configureEach { + compileTaskProvider.get().compilerOptions { + freeCompilerArgs.add("-Xexpect-actual-classes") + } + } + } + jvm() androidTarget { publishLibraryVariants("release") diff --git a/model-zoo/build.gradle.kts b/model-zoo/build.gradle.kts index 0c8f1693..b5d88916 100644 --- a/model-zoo/build.gradle.kts +++ b/model-zoo/build.gradle.kts @@ -8,6 +8,14 @@ plugins { } kotlin { + targets.configureEach { + compilations.configureEach { + compileTaskProvider.get().compilerOptions { + freeCompilerArgs.add("-Xexpect-actual-classes") + } + } + } + jvm() androidTarget { compilations.all { @@ -71,14 +79,7 @@ kotlin { } } - val iosX64Main by getting - val iosArm64Main by getting - val iosSimulatorArm64Main by getting val iosMain by creating { - dependsOn(commonMain) - iosX64Main.dependsOn(this) - iosArm64Main.dependsOn(this) - iosSimulatorArm64Main.dependsOn(this) dependencies { implementation("io.ktor:ktor-client-darwin:3.1.3") } diff --git a/model-zoo/src/jvmMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoader.kt b/model-zoo/src/jvmMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoader.kt deleted file mode 100644 index 940a7478..00000000 --- a/model-zoo/src/jvmMain/kotlin/sk/ai/net/io/data/mnist/MNISTLoader.kt +++ /dev/null @@ -1,112 +0,0 @@ -package sk.ai.net.io.data.mnist; - - - -import io.ktor.client.HttpClient -import io.ktor.client.engine.cio.CIO -import io.ktor.client.plugins.logging.Logging -import io.ktor.client.request.get -import io.ktor.client.request.headers -import io.ktor.utils.io.jvm.javaio.copyTo -import io.ktor.utils.io.core.use -import io.ktor.client.call.body -import io.ktor.client.plugins.onDownload -import io.ktor.client.plugins.contentnegotiation.ContentNegotiation -import io.ktor.client.request.get -import io.ktor.client.statement.bodyAsChannel -import io.ktor.utils.io.copyAndClose -import java.io.File -import java.io.FileInputStream -import java.io.FileOutputStream -import java.util.zip.GZIPInputStream - -import kotlinx.coroutines.runBlocking -import java.nio.file.Files -import java.nio.file.Paths -import java.util.zip.GZIPOutputStream - - - - - - -import io.ktor.client.statement.HttpResponse - -suspend fun downloadFile(urlStr: String, outputPath: String) { - val client = HttpClient(CIO) { - install(Logging) - } - // This is the correct way to initialize the HttpClient with CIO - try { - val file = File(outputPath) - - val httpClient = HttpClient { - install(Logging) - } - - val httpResponse: HttpResponse = client.get(urlStr) { - onDownload { bytesSentTotal, contentLength -> - println("Received $bytesSentTotal bytes from $contentLength") - } - } - val responseBody: ByteArray = httpResponse.body() - file.writeBytes(responseBody) - println("A file saved to ${file.path}") - - } finally { - client.close() - } -} - - - - -fun decompressGzipFile(gzipFilePath: String, outputFilePath: String) { - GZIPInputStream(FileInputStream(gzipFilePath)).use { gzipInputStream -> - FileOutputStream(outputFilePath).use { outputStream -> - val buffer = ByteArray(1024) - var len: Int - while (gzipInputStream.read(buffer).also { len = it } > 0) { - outputStream.write(buffer, 0, len) - } - } - } - println("Decompressed $gzipFilePath to $outputFilePath") -} - - -fun downloadMnistDataset() = runBlocking { - val urls = mapOf( - "train-images-idx3-ubyte.gz" to "https://ossci-datasets.s3.amazonaws.com/mnist/train-images-idx3-ubyte.gz", - "train-labels-idx1-ubyte.gz" to "https://ossci-datasets.s3.amazonaws.com/mnist/train-labels-idx1-ubyte.gz", - "t10k-images-idx3-ubyte.gz" to "https://ossci-datasets.s3.amazonaws.com/mnist/t10k-images-idx3-ubyte.gz", - "t10k-labels-idx1-ubyte.gz" to "https://ossci-datasets.s3.amazonaws.com/mnist/t10k-labels-idx1-ubyte.gz" - ) - - // https://ossci-datasets.s3.amazonaws.com/mnist/train-images-idx3-ubyte.gz - - urls.forEach { (fileName, url) -> - val outputPath = Paths.get(fileName).toString() - if (!Files.exists(Paths.get(outputPath))) { - downloadFile(url, outputPath) - } else { - println("$fileName already exists. Skipping download.") - } - } -} - -fun decompress() { - val gzipFiles = listOf( - "train-images-idx3-ubyte.gz", - "train-labels-idx1-ubyte.gz", - "t10k-images-idx3-ubyte.gz", - "t10k-labels-idx1-ubyte.gz" - ) - - gzipFiles.forEach { gzipFile -> - val outputFile = gzipFile.removeSuffix(".gz") - decompressGzipFile(gzipFile, outputFile) - } -} - - From bb988f59d2c07b475ed778b27b02f6a5d9fb40be Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Wed, 18 Jun 2025 18:00:08 +0200 Subject: [PATCH 16/19] Add AvgPool2d Related-To: #47 --- .../kotlin/sk/ai/net/nn/AvgPool2d.kt | 72 +++++++++++++++++++ .../kotlin/sk/ai/net/nn/AvgPool2dTest.kt | 44 ++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 core/src/commonMain/kotlin/sk/ai/net/nn/AvgPool2d.kt create mode 100644 core/src/commonTest/kotlin/sk/ai/net/nn/AvgPool2dTest.kt diff --git a/core/src/commonMain/kotlin/sk/ai/net/nn/AvgPool2d.kt b/core/src/commonMain/kotlin/sk/ai/net/nn/AvgPool2d.kt new file mode 100644 index 00000000..f6d0be3a --- /dev/null +++ b/core/src/commonMain/kotlin/sk/ai/net/nn/AvgPool2d.kt @@ -0,0 +1,72 @@ +package sk.ai.net.nn + +import sk.ai.net.Shape +import sk.ai.net.Tensor +import sk.ai.net.impl.DoublesTensor + +/** + * 2D average pooling layer. + * Works with tensors of shape (N, C, H, W) or (C, H, W). + */ +class AvgPool2d( + val kernelSize: Int, + val stride: Int = kernelSize, + override val name: String = "AvgPool2d" +) : Module() { + override val modules: List + get() = emptyList() + + override fun forward(input: Tensor): Tensor = avgPool2d(input) + + private fun avgPool2d(input: Tensor): Tensor { + val tensor = input as DoublesTensor + val shape = tensor.shape + require(shape.rank == 3 || shape.rank == 4) { + "AvgPool2d expected 3D or 4D input tensor, but got shape $shape" + } + val batchSize: Int + val channels: Int + val height: Int + val width: Int + if (shape.rank == 4) { + batchSize = shape.dimensions[0] + channels = shape.dimensions[1] + height = shape.dimensions[2] + width = shape.dimensions[3] + } else { + batchSize = 1 + channels = shape.dimensions[0] + height = shape.dimensions[1] + width = shape.dimensions[2] + } + + val outH = (height - kernelSize) / stride + 1 + val outW = (width - kernelSize) / stride + 1 + val outElements = DoubleArray(batchSize * channels * outH * outW) + var idx = 0 + for (n in 0 until batchSize) { + for (c in 0 until channels) { + for (i in 0 until outH) { + for (j in 0 until outW) { + var sum = 0.0 + for (ki in 0 until kernelSize) { + for (kj in 0 until kernelSize) { + val h = i * stride + ki + val w = j * stride + kj + val value = if (shape.rank == 4) { + tensor.elements[n * channels * height * width + c * height * width + h * width + w] + } else { + tensor.elements[c * height * width + h * width + w] + } + sum += value + } + } + outElements[idx++] = sum / (kernelSize * kernelSize) + } + } + } + } + val outShape = Shape(batchSize, channels, outH, outW) + return DoublesTensor(outShape, outElements) + } +} \ No newline at end of file diff --git a/core/src/commonTest/kotlin/sk/ai/net/nn/AvgPool2dTest.kt b/core/src/commonTest/kotlin/sk/ai/net/nn/AvgPool2dTest.kt new file mode 100644 index 00000000..52d8d629 --- /dev/null +++ b/core/src/commonTest/kotlin/sk/ai/net/nn/AvgPool2dTest.kt @@ -0,0 +1,44 @@ +package sk.ai.net.nn + +import sk.ai.net.Shape +import sk.ai.net.dsl.network +import sk.ai.net.impl.DoublesTensor +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class AvgPool2dTest { + @Test + fun avg_pool2d_basic() { + val input = DoublesTensor( + Shape(1, 1, 4, 4), + doubleArrayOf( + 1.0, 2.0, 3.0, 4.0, + 5.0, 6.0, 7.0, 8.0, + 9.0, 10.0, 11.0, 12.0, + 13.0, 14.0, 15.0, 16.0 + ) + ) + val pool = AvgPool2d(kernelSize = 2, stride = 2) + val result = pool.forward(input) as DoublesTensor + assertEquals(Shape(1, 1, 2, 2), result.shape) + assertContentEquals(doubleArrayOf(3.5, 5.5, 11.5, 13.5), result.elements) + } + + @Test + fun dsl_support() { + val module = network { + input(1) + avgPool2d { + kernelSize = 2 + stride = 2 + } + } + val mlp = module as sk.ai.net.nn.topology.MLP + assertTrue(mlp.modules[1] is AvgPool2d) + val ap = mlp.modules[1] as AvgPool2d + assertEquals(2, ap.kernelSize) + assertEquals(2, ap.stride) + } +} \ No newline at end of file From df21cc9cb67dc9fec908aaca936c1914101dc418 Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Wed, 18 Jun 2025 18:00:21 +0200 Subject: [PATCH 17/19] Add AvgPool2d DSL Related-To: #47 --- .../kotlin/sk/ai/net/dsl/NetworkBuilder.kt | 45 +++++++++++++++++-- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/core/src/commonMain/kotlin/sk/ai/net/dsl/NetworkBuilder.kt b/core/src/commonMain/kotlin/sk/ai/net/dsl/NetworkBuilder.kt index 4511ab38..277a2e00 100644 --- a/core/src/commonMain/kotlin/sk/ai/net/dsl/NetworkBuilder.kt +++ b/core/src/commonMain/kotlin/sk/ai/net/dsl/NetworkBuilder.kt @@ -8,6 +8,7 @@ import sk.ai.net.nn.Input import sk.ai.net.nn.Linear import sk.ai.net.nn.Conv2d import sk.ai.net.nn.MaxPool2d +import sk.ai.net.nn.AvgPool2d import sk.ai.net.nn.Module import sk.ai.net.nn.topology.MLP @@ -34,14 +35,16 @@ interface NeuralNetworkDsl : NetworkDslItem { fun maxPool2d(id: String = "", content: MAXPOOL2D.() -> Unit = {}) + fun avgPool2d(id: String = "", content: AVGPOOL2D.() -> Unit = {}) + fun dense(outputDimension: Int, id: String = "", content: DENSE.() -> Unit = {}) - + fun dense(id: String = "", content: DENSE.() -> Unit = {}) fun activation(id: String = "", activation: (Tensor) -> Tensor) - + fun sequential(content: NeuralNetworkDsl.() -> Unit) - + fun stage(id: String, content: NeuralNetworkDsl.() -> Unit) } @@ -73,6 +76,12 @@ interface MAXPOOL2D : NetworkDslItem { var stride: Int } +@NetworkDsl +interface AVGPOOL2D : NetworkDslItem { + var kernelSize: Int + var stride: Int +} + private fun getDefaultName(id: String, s: String, size: Int): String { if (id.isNotEmpty()) return id @@ -196,6 +205,18 @@ class MaxPool2dImpl( ) } +class AvgPool2dImpl( + override var kernelSize: Int = 2, + override var stride: Int = 2, + private val id: String +) : AVGPOOL2D { + fun create(): Module = AvgPool2d( + kernelSize = kernelSize, + stride = stride, + name = id + ) +} + // Stage implementation class StageImpl(private val id: String) : NeuralNetworkDsl { val modules = mutableListOf() @@ -235,6 +256,14 @@ class StageImpl(private val id: String) : NeuralNetworkDsl { modules += impl.create() } + override fun avgPool2d(id: String, content: AVGPOOL2D.() -> Unit) { + val impl = AvgPool2dImpl( + id = getDefaultName(id, "avgPool2d", modules.size) + ) + impl.content() + modules += impl.create() + } + override fun dense(outputDimension: Int, id: String, content: DENSE.() -> Unit) { val inputDimension = lastDimension lastDimension = outputDimension @@ -321,6 +350,14 @@ private class NeuralNetworkDslImpl : NeuralNetworkDsl { modules += impl.create() } + override fun avgPool2d(id: String, content: AVGPOOL2D.() -> Unit) { + val impl = AvgPool2dImpl( + id = getDefaultName(id, "avgPool2d", modules.size) + ) + impl.content() + modules += impl.create() + } + override fun dense(outputDimension: Int, id: String, content: DENSE.() -> Unit) { val inputDimension = lastDimension lastDimension = outputDimension @@ -380,4 +417,4 @@ class NetworkBuilder { } fun build(): Module = MLP(*modules.toTypedArray()) -} \ No newline at end of file +} From 2904cbba0823aa4762d31fa83e567c0d1d666837 Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Wed, 18 Jun 2025 18:16:37 +0200 Subject: [PATCH 18/19] Add matmul to tensors with NCWH layout Related-To: #47 --- .../kotlin/sk/ai/net/impl/DoublesTensor.kt | 84 +++++++++- .../commonTest/kotlin/sk/ai/net/TensorTest.kt | 146 ++++++++++++++++++ 2 files changed, 225 insertions(+), 5 deletions(-) diff --git a/core/src/commonMain/kotlin/sk/ai/net/impl/DoublesTensor.kt b/core/src/commonMain/kotlin/sk/ai/net/impl/DoublesTensor.kt index 498f600f..412e1d00 100644 --- a/core/src/commonMain/kotlin/sk/ai/net/impl/DoublesTensor.kt +++ b/core/src/commonMain/kotlin/sk/ai/net/impl/DoublesTensor.kt @@ -5,13 +5,9 @@ import sk.ai.net.Shape import sk.ai.net.Tensor import sk.ai.net.core.Slice import sk.ai.net.core.TypedTensor -import sk.ai.net.core.end -import sk.ai.net.core.start import kotlin.collections.map import kotlin.math.exp import kotlin.math.pow -import kotlin.random.Random -import kotlin.text.toInt data class DoublesTensor(override val shape: Shape, val elements: DoubleArray) : TypedTensor { constructor(shape: Shape, element: Double = 0.0) : this( @@ -282,7 +278,85 @@ data class DoublesTensor(override val shape: Shape, val elements: DoubleArray) : return DoublesTensor(newShape, result) } - throw IllegalArgumentException("Unsupported tensor shapes for multiplication.") + // 4D NCHW tensor and 2D matrix multiplication (for CNN operations) + if (shape.dimensions.size == 4 && other.shape.dimensions.size == 2) { + val (batchSize, channels, height, width) = shape.dimensions + val (inputFeatures, outputFeatures) = other.shape.dimensions + + // Check if the last dimension of the 4D tensor matches the first dimension of the 2D tensor + if (width != inputFeatures) throw IllegalArgumentException( + "Shapes do not align for 4D-2D multiplication. Expected width=$inputFeatures, got width=$width" + ) + + // Result shape will be [batchSize, channels, height, outputFeatures] + val newShape = Shape(batchSize, channels, height, outputFeatures) + val result = DoubleArray(newShape.volume) { 0.0 } + + val otherTensor = other as DoublesTensor + + // Perform the matrix multiplication + var resultIdx = 0 + for (n in 0 until batchSize) { + for (c in 0 until channels) { + for (h in 0 until height) { + // Get the row from the 4D tensor + val rowStartIdx = ((n * channels + c) * height + h) * width + val row = DoubleArray(width) { w -> elements[rowStartIdx + w] } + + // Multiply the row by each column of the weight matrix + for (outIdx in 0 until outputFeatures) { + var sum = 0.0 + for (w in 0 until width) { + sum += row[w] * otherTensor.elements[w * outputFeatures + outIdx] + } + result[resultIdx++] = sum + } + } + } + } + + return DoublesTensor(newShape, result) + } + + // 3D tensor and 2D matrix multiplication (for CNN operations without batch dimension) + if (shape.dimensions.size == 3 && other.shape.dimensions.size == 2) { + val (channels, height, width) = shape.dimensions + val (inputFeatures, outputFeatures) = other.shape.dimensions + + // Check if the last dimension of the 3D tensor matches the first dimension of the 2D tensor + if (width != inputFeatures) throw IllegalArgumentException( + "Shapes do not align for 3D-2D multiplication. Expected width=$inputFeatures, got width=$width" + ) + + // Result shape will be [channels, height, outputFeatures] + val newShape = Shape(channels, height, outputFeatures) + val result = DoubleArray(newShape.volume) { 0.0 } + + val otherTensor = other as DoublesTensor + + // Perform the matrix multiplication + var resultIdx = 0 + for (c in 0 until channels) { + for (h in 0 until height) { + // Get the row from the 3D tensor + val rowStartIdx = (c * height + h) * width + val row = DoubleArray(width) { w -> elements[rowStartIdx + w] } + + // Multiply the row by each column of the weight matrix + for (outIdx in 0 until outputFeatures) { + var sum = 0.0 + for (w in 0 until width) { + sum += row[w] * otherTensor.elements[w * outputFeatures + outIdx] + } + result[resultIdx++] = sum + } + } + } + + return DoublesTensor(newShape, result) + } + + throw IllegalArgumentException("Unsupported tensor shapes for multiplication: ${shape.dimensions.toList()} and ${other.shape.dimensions.toList()}") } override fun t(): Tensor { diff --git a/core/src/commonTest/kotlin/sk/ai/net/TensorTest.kt b/core/src/commonTest/kotlin/sk/ai/net/TensorTest.kt index 6737f7f7..0b7fb29a 100644 --- a/core/src/commonTest/kotlin/sk/ai/net/TensorTest.kt +++ b/core/src/commonTest/kotlin/sk/ai/net/TensorTest.kt @@ -388,6 +388,152 @@ class TensorTest { ) { lhs: Double, rhs: Double -> lhs - rhs } assertFalse(a.any { it > 1e-4 }) + } + + @Test + fun matmulOf4DNCHWTensorAnd2DMatrix() { + // Create a 4D NCHW tensor with shape [2, 3, 2, 4] + // 2 batches, 3 channels, 2 height, 4 width + val tensor4D = createTensor( + Shape(2, 3, 2, 4), + doubleArrayOf( + // Batch 0, Channel 0 + 1.0, 2.0, 3.0, 4.0, // Row 0 + 5.0, 6.0, 7.0, 8.0, // Row 1 + + // Batch 0, Channel 1 + 9.0, 10.0, 11.0, 12.0, // Row 0 + 13.0, 14.0, 15.0, 16.0, // Row 1 + + // Batch 0, Channel 2 + 17.0, 18.0, 19.0, 20.0, // Row 0 + 21.0, 22.0, 23.0, 24.0, // Row 1 + + // Batch 1, Channel 0 + 25.0, 26.0, 27.0, 28.0, // Row 0 + 29.0, 30.0, 31.0, 32.0, // Row 1 + + // Batch 1, Channel 1 + 33.0, 34.0, 35.0, 36.0, // Row 0 + 37.0, 38.0, 39.0, 40.0, // Row 1 + + // Batch 1, Channel 2 + 41.0, 42.0, 43.0, 44.0, // Row 0 + 45.0, 46.0, 47.0, 48.0 // Row 1 + ) + ) + + // Create a 2D matrix with shape [4, 3] + // 4 input features, 3 output features + val matrix2D = createTensor( + Shape(4, 3), + doubleArrayOf( + 0.1, 0.2, 0.3, // Row 0 + 0.4, 0.5, 0.6, // Row 1 + 0.7, 0.8, 0.9, // Row 2 + 1.0, 1.1, 1.2 // Row 3 + ) + ) + + // Perform matrix multiplication + val result = tensor4D.matmul(matrix2D) + + // Check the shape of the result + assertEquals(Shape(2, 3, 2, 3), result.shape) + + // Expected result calculation: + // For batch 0, channel 0, row 0: + // [1.0, 2.0, 3.0, 4.0] × [0.1, 0.2, 0.3; 0.4, 0.5, 0.6; 0.7, 0.8, 0.9; 1.0, 1.1, 1.2] + // = [1.0*0.1 + 2.0*0.4 + 3.0*0.7 + 4.0*1.0, 1.0*0.2 + 2.0*0.5 + 3.0*0.8 + 4.0*1.1, 1.0*0.3 + 2.0*0.6 + 3.0*0.9 + 4.0*1.2] + // = [0.1 + 0.8 + 2.1 + 4.0, 0.2 + 1.0 + 2.4 + 4.4, 0.3 + 1.2 + 2.7 + 4.8] + // = [7.0, 8.0, 9.0] + + // Expected values for the first few elements + val expectedFirstBatch = doubleArrayOf( + // Batch 0, Channel 0 + 7.0, 8.0, 9.0, // Row 0 + 15.8, 18.4, 21.0, // Row 1 + + // Batch 0, Channel 1 + 24.6, 28.8, 33.0, // Row 0 + 33.4, 39.2, 45.0, // Row 1 + + // Batch 0, Channel 2 + 42.2, 49.6, 57.0, // Row 0 + 51.0, 60.0, 69.0 // Row 1 + ) + + // Check the first batch values + for (i in expectedFirstBatch.indices) { + assertEquals(expectedFirstBatch[i], (result as DoublesTensor).elements[i], 1e-10) + } + } + + @Test + fun matmulOf3DTensorAnd2DMatrix() { + // Create a 3D tensor with shape [3, 2, 4] + // 3 channels, 2 height, 4 width + val tensor3D = createTensor( + Shape(3, 2, 4), + doubleArrayOf( + // Channel 0 + 1.0, 2.0, 3.0, 4.0, // Row 0 + 5.0, 6.0, 7.0, 8.0, // Row 1 + + // Channel 1 + 9.0, 10.0, 11.0, 12.0, // Row 0 + 13.0, 14.0, 15.0, 16.0, // Row 1 + + // Channel 2 + 17.0, 18.0, 19.0, 20.0, // Row 0 + 21.0, 22.0, 23.0, 24.0 // Row 1 + ) + ) + + // Create a 2D matrix with shape [4, 3] + // 4 input features, 3 output features + val matrix2D = createTensor( + Shape(4, 3), + doubleArrayOf( + 0.1, 0.2, 0.3, // Row 0 + 0.4, 0.5, 0.6, // Row 1 + 0.7, 0.8, 0.9, // Row 2 + 1.0, 1.1, 1.2 // Row 3 + ) + ) + + // Perform matrix multiplication + val result = tensor3D.matmul(matrix2D) + + // Check the shape of the result + assertEquals(Shape(3, 2, 3), result.shape) + + // Expected result calculation: + // For channel 0, row 0: + // [1.0, 2.0, 3.0, 4.0] × [0.1, 0.2, 0.3; 0.4, 0.5, 0.6; 0.7, 0.8, 0.9; 1.0, 1.1, 1.2] + // = [1.0*0.1 + 2.0*0.4 + 3.0*0.7 + 4.0*1.0, 1.0*0.2 + 2.0*0.5 + 3.0*0.8 + 4.0*1.1, 1.0*0.3 + 2.0*0.6 + 3.0*0.9 + 4.0*1.2] + // = [0.1 + 0.8 + 2.1 + 4.0, 0.2 + 1.0 + 2.4 + 4.4, 0.3 + 1.2 + 2.7 + 4.8] + // = [7.0, 8.0, 9.0] + + // Expected values for all elements + val expectedValues = doubleArrayOf( + // Channel 0 + 7.0, 8.0, 9.0, // Row 0 + 15.8, 18.4, 21.0, // Row 1 + + // Channel 1 + 24.6, 28.8, 33.0, // Row 0 + 33.4, 39.2, 45.0, // Row 1 + + // Channel 2 + 42.2, 49.6, 57.0, // Row 0 + 51.0, 60.0, 69.0 // Row 1 + ) + // Check all values with a small tolerance for floating-point precision + val resultElements = (result as DoublesTensor).elements + for (i in expectedValues.indices) { + assertEquals(expectedValues[i], resultElements[i], 1e-10) + } } } From dd34454d46d40d4beb731807fbbda32f00328a18 Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Sat, 21 Jun 2025 13:11:27 +0200 Subject: [PATCH 19/19] Add release version --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index e5faf253..534ff9a3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,7 +10,7 @@ plugins { allprojects { group = "sk.ai.net" - version = "0.0.6.1" + version = "0.0.7" } moduleGraphConfig {