diff --git a/alchemist-loading/build.gradle.kts b/alchemist-loading/build.gradle.kts index f7d25a6b9c..24bf2ee36c 100644 --- a/alchemist-loading/build.gradle.kts +++ b/alchemist-loading/build.gradle.kts @@ -38,8 +38,11 @@ dependencies { implementation(libs.kasechange) implementation(libs.kotlin.reflect) implementation(libs.kotlin.coroutines.core) + implementation(libs.kotlinx.serialization.json) + implementation(libs.rrmxmx) implementation(libs.mongodb) implementation(libs.snakeyaml) + implementation(libs.caffeine) runtimeOnly(libs.groovy.jsr223) runtimeOnly(kotlin("scripting-jsr223")) @@ -50,6 +53,7 @@ dependencies { testImplementation(libs.appdirs) testImplementation(libs.caffeine) testImplementation(libs.embedmongo) + testImplementation(libs.kotlin.test) testRuntimeOnly(incarnation("sapere")) testRuntimeOnly(incarnation("protelis")) } diff --git a/alchemist-loading/src/main/kotlin/it/unibo/alchemist/boundary/NelderMeadMethod.kt b/alchemist-loading/src/main/kotlin/it/unibo/alchemist/boundary/NelderMeadMethod.kt new file mode 100644 index 0000000000..dedf7f8f8d --- /dev/null +++ b/alchemist-loading/src/main/kotlin/it/unibo/alchemist/boundary/NelderMeadMethod.kt @@ -0,0 +1,181 @@ +package it.unibo.alchemist.boundary + +import com.github.benmanes.caffeine.cache.Caffeine +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.Future + +/** + * Nelder-Mead optimization method. + * Given an initial [simplex], this method iteratively refines the simplex to minimize a given [objective] function. + * The method is suitable for optimizing functions that are continuous but not differentiable. + * Other parameters are: + * - [alpha]: Reflection coefficient (standard value is 1.0); + * - [gamma]: Expansion coefficient (standard value is 2.0); + * - [rho]: Contraction coefficient (standard value is 0.5); + * - [sigma]: Shrink coefficient (standard value is 0.5); + * - [maxIterations]: Maximum number of iterations; + * - [tolerance]: Termination condition (small variation in function values). + */ +class NelderMeadMethod( + val simplex: List, + private val maxIterations: Int, + private val tolerance: Double, + private val alpha: Double, // Reflection coefficient + private val gamma: Double, // Expansion coefficient + private val rho: Double, // Contraction coefficient + private val sigma: Double, // Shrink coefficient + executorService: ExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()), + private val objective: (List) -> Future, +) { + + private val cache = Caffeine.newBuilder().executor(executorService) + .build, Future> { coordinates -> + objective(coordinates) + } + + /** + * Apply the Nelder-Mead optimization method to the given [simplex] and [objective] function. + */ + fun optimize(): Vertex { + require(simplex.isNotEmpty()) { "The initial simplex must not be empty" } + val dimensions = simplex.first().size + require(dimensions > 0) { "The number of dimensions must be greater than 0" } + require(simplex.size == dimensions + 1) { + "The vertices of the initial simplex must be one more than the number of dimensions" + } + require(simplex.all { it.size == dimensions }) { + "All vertices of the initial simplex must have the same number of dimensions" + } + var symplexUpdated = simplex + repeat(maxIterations) { + // Sort simplex by function values + val sortedSimplex = symplexUpdated + .map { it to cache[it.valuesToList()] } + .sortedBy { it.second.get() } + .map { it.first } + val bestVertex: Vertex = sortedSimplex.first() + val worstVertex: Vertex = sortedSimplex.last() + val secondWorstVertex: Vertex = sortedSimplex[simplex.size - 2] + val bestValue = cache[bestVertex.valuesToList()] + val worstValues = worstVertex.valuesToList() + // Compute centroid (excluding worst point) + val centroid = + DoubleArray(dimensions) { index -> + sortedSimplex.dropLast(1).sumOf { it[index] } / (sortedSimplex.size - 1) + }.toList() + // Reflections + val reflected: List = centroid.mapCentroid(alpha, worstValues) + val reflectedValue = cache[reflected] + require(reflectedValue.get().isFinite() && !reflectedValue.get().isNaN()) { + "Invalid objective function return value for reflection with $reflected = $reflectedValue.\n" + + "Check the objective function implementation, the result should be a finite number." + } +// check (!reflectedValue.get().isFinite()) { +// error("Invalid objective function return value for reflection") +// } + val newSimplex = when { + reflectedValue < bestValue -> { // expansion + val expanded: List = centroid.mapCentroid(gamma, reflected) + when { + cache[expanded] < reflectedValue -> sortedSimplex.updateLastVertex(expanded) + else -> sortedSimplex.updateLastVertex(reflected) + } + } + reflectedValue < cache[secondWorstVertex.valuesToList()] -> { // accept reflection + sortedSimplex.updateLastVertex(reflected) + } + else -> { // contraction + val contracted = when { + reflectedValue < cache[worstValues] -> centroid.mapCentroid(rho, reflected) + else -> centroid.mapCentroid(rho, worstValues) + } + when { + cache[contracted] < cache[worstValues] -> sortedSimplex.updateLastVertex(contracted) + else -> { // shrink simplex + sortedSimplex.map { vertex -> + Vertex( + vertex.keys().associateWith { key -> + // Find the index corresponding to the key + val index = bestVertex.keys().indexOf(key) + val oldValue = bestVertex[index] + oldValue + sigma * (vertex[index] - oldValue) // Apply shrink transformation + }, + ) + } + } + } + } + } + // Check termination condition (small variation in function values) + val functionValues = newSimplex.map { cache[it.valuesToList()].get() } + symplexUpdated = newSimplex + val maxDiff = functionValues.maxOrNull()!! - functionValues.minOrNull()!! + if (maxDiff < tolerance) return symplexUpdated.first() + } + return symplexUpdated.first() + } + + private fun List.mapCentroid(coefficient: Double, values: List): List = + mapIndexed { index, value -> value + coefficient * (values[index] - value) } + + private fun List.updateLastVertex(newVertex: List): List = mapIndexed { index, vertex -> + if (index == size - 1) { + Vertex(vertex.keys().associateWith { newVertex[vertex.keys().indexOf(it)] }) + } else { + vertex + } + } + + /** + * This companion object contains the comparison operators for [Future] of [Double] objects and [Double]s. + */ + companion object { + /** + * Compares two [Future] of [Double] objects. + */ + operator fun Future.compareTo(other: Future): Int = get().compareTo(other.get()) + + /** + * Compares a [Future] of [Double] object with a [Double]. + */ + operator fun Future.compareTo(other: Double): Int = get().compareTo(other) + + /** + * Compares a [Double] with a [Future] of [Double] object. + */ + operator fun Double.compareTo(other: Future): Int = compareTo(other.get()) + } +} + +/** + * A vertex of the simplex in the Nelder-Mead method. + */ +@JvmInline +value class Vertex(private val vertex: Map) { + /** + * Returns amount of dimensions of the vertex. + */ + val size: Int + get() = vertex.size + + /** + * Returns the values of the vertex as a list. + */ + fun valuesToList(): List = vertex.values.toList() + + /** + * Returns the keys of the vertex as a set. + */ + fun keys(): Set = vertex.keys + + /** + * Returns the value of the vertex at the given index. + */ + operator fun get(index: Int) = valuesToList()[index] + + /** + * Returns the key of the vertex at the given index. + */ + fun keyAt(index: Int) = vertex.keys.elementAt(index) +} diff --git a/alchemist-loading/src/main/kotlin/it/unibo/alchemist/boundary/launchers/NelderMeadLauncher.kt b/alchemist-loading/src/main/kotlin/it/unibo/alchemist/boundary/launchers/NelderMeadLauncher.kt new file mode 100644 index 0000000000..5c18bb7aff --- /dev/null +++ b/alchemist-loading/src/main/kotlin/it/unibo/alchemist/boundary/launchers/NelderMeadLauncher.kt @@ -0,0 +1,166 @@ +package it.unibo.alchemist.boundary.launchers + +import it.unibo.alchemist.boundary.Launcher +import it.unibo.alchemist.boundary.Loader +import it.unibo.alchemist.boundary.NelderMeadMethod +import it.unibo.alchemist.boundary.Variable +import it.unibo.alchemist.boundary.Vertex +import it.unibo.alchemist.model.Environment +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject +import org.danilopianini.rrmxmx.RrmxmxRandom +import org.danilopianini.rrmxmx.RrmxmxRandom.Companion.DEFAULT_SEED +import java.io.File +import java.time.LocalDateTime +import java.util.concurrent.Callable +import java.util.concurrent.ConcurrentLinkedDeque +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.ForkJoinPool +import java.util.concurrent.Future +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger +import kotlin.Int.Companion.MAX_VALUE + +/** + * Alchemist launcher for the Nelder-Mead method optimization. + * This launcher optimize the [variables] of the simulation by a given [objectiveFunction]. + * The optimization is done by the Nelder-Mead method, which is a derivative-free optimization algorithm. + * The optimization is done in parallel for a given number of [repetitions] of the [seedName] and [maxIterations]. + * It will stop when the [tolerance] is reached. + * The [alpha], [gamma], [rho] and [sigma] parameters are the standard values for the Nelder-Mead method. + * The result of the optimization is written into `[outputPath]` folder in [outputFileName] as a JSON file. + */ +class NelderMeadLauncher +@JvmOverloads +constructor( + private val outputPath: String = "data", + private val outputFileName: String = "nelderMead", + private val objectiveFunction: Environment<*, *>.() -> Double, + private val variables: List = emptyList(), + private val seedName: String, + private val repetitions: Int = 1, + private val maxIterations: Int = MAX_VALUE, + private val seed: ULong = DEFAULT_SEED, + private val tolerance: Double = 1e-6, + private val alpha: Double = 1.0, // standard value for the reflection in Nelder-Mead method + private val gamma: Double = 2.0, // standard value for the expansion in Nelder-Mead method + private val rho: Double = 0.5, // standard value for the contraction in Nelder-Mead method + private val sigma: Double = 0.5, // standard value for the shrink in Nelder-Mead method +) : Launcher { + @OptIn(ExperimentalSerializationApi::class) + @Synchronized + override fun launch(loader: Loader) { + require(loader.variables.isNotEmpty() || variables.isNotEmpty()) { + "No variables found, can not optimize anything." + } + val simplexVertices: List> = generateSymplexVertices(loader.variables) + val seeds: List = + loader.variables[seedName] + ?.stream() + ?.map { + check(it is Number) { "Seed must be a number. $it is not." } + it.toInt() + }?.toList() + ?.take(repetitions) ?: listOf(repetitions) + val executorID = AtomicInteger(0) + val executor = + Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()) { + Thread(it, "Alchemist Nelder Mead worker #${executorID.getAndIncrement()}") + } + val errorQueue = ConcurrentLinkedDeque() + loader.executeWithNelderMead(simplexVertices, executor) { vertex -> + val futureValues = seeds.map> { currentSeed -> + // associate keys to vertex values + val simulationParameters = variables + .associateWith { vertex[variables.indexOf(it)] } + (seedName to currentSeed) + check(loader.variables.keys == simulationParameters.keys) { + "Variables do not match: ${loader.variables.keys} != ${simulationParameters.keys}" + } + executor.submit( + Callable { + val simulation = loader.getWith(simulationParameters) + simulation.play() + simulation.run() + if (simulation.error.isPresent) { + errorQueue.add(simulation.error.get()) + } + objectiveFunction(simulation.environment) + }, + ) + } + ForkJoinPool.commonPool().submit( + Callable { + futureValues.map { it.get() }.average() + }, + ) + }.also { result -> + // if not exists create the directory + File(outputPath).mkdirs() + val outputFile = File( + "$outputPath${File.separator}${outputFileName}" + + "_${LocalDateTime.now().toString().replace(":", "-")}.json", + ) + val outputContent = buildJsonObject { + put(seedName, JsonPrimitive(repetitions)) + put("variables", buildJsonObject { + variables.forEach { variable -> + put(variable, JsonPrimitive(result[variable])) + } + }) + }.toString() + outputFile.writeText(outputContent) + } + executor.shutdown() + executor.awaitTermination(Long.MAX_VALUE, TimeUnit.HOURS) + if (errorQueue.isNotEmpty()) { + throw errorQueue.reduce { previous, other -> + previous.addSuppressed(other) + previous + } + } + } + + private fun Loader.executeWithNelderMead( + simplexVertices: List>, + executorService: ExecutorService, + executeFunction: (List) -> Future, + ): Map = NelderMeadMethod( + simplex = simplexVertices.map { Vertex(it) }, + maxIterations = maxIterations, + tolerance = tolerance, + alpha = alpha, + gamma = gamma, + rho = rho, + sigma = sigma, + executorService = executorService, + objective = executeFunction, + ).optimize() + .let { result -> + this@NelderMeadLauncher.variables.associateWith { + result[this@NelderMeadLauncher.variables.indexOf(it)] + } + } + + private fun generateSymplexVertices(loaderVariables: Map>): List> { + val randomGenerator = RrmxmxRandom(seed) + val instances: Map> = + variables.associateWith { varName -> + val variable = loaderVariables.getValue(varName) + val allValues = + variable + .stream() + .map { + check(it is Number) { + "All variables to optimize must be Numbers. $varName has value $it." + } + it.toDouble() + }.toList() + allValues.min()..allValues.max() + } + return (0..variables.size).map { + instances.mapValues { (_, range) -> randomGenerator.nextDouble(range.start, range.endInclusive) } + } + } +} diff --git a/alchemist-loading/src/test/kotlin/it/unibo/alchemist/test/TestNelderMeadLauncher.kt b/alchemist-loading/src/test/kotlin/it/unibo/alchemist/test/TestNelderMeadLauncher.kt new file mode 100644 index 0000000000..268c6f64c6 --- /dev/null +++ b/alchemist-loading/src/test/kotlin/it/unibo/alchemist/test/TestNelderMeadLauncher.kt @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2010-2025, Danilo Pianini and contributors + * listed, for each module, in the respective subproject's build.gradle.kts file. + * + * This file is part of Alchemist, and is distributed under the terms of the + * GNU General Public License, with a linking exception, + * as described in the file LICENSE in the Alchemist distribution's top directory. + */ + +package it.unibo.alchemist.test + +import it.unibo.alchemist.boundary.LoadAlchemist +import it.unibo.alchemist.boundary.launchers.DefaultLauncher +import it.unibo.alchemist.core.Status +import it.unibo.alchemist.model.Node +import it.unibo.alchemist.model.Environment +import it.unibo.alchemist.model.Position +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonObject +import org.kaikikm.threadresloader.ResourceLoader +import java.awt.geom.QuadCurve2D +import java.io.File +import java.util.concurrent.TimeUnit +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class TestNelderMeadLauncher() { + val loader = LoadAlchemist + .from(ResourceLoader.getResource("testNelderMeadLauncher.yml")) + + @Test + fun `Nelder mead launcher should optimize the objective function as expected`() { + val launcher = loader.launcher + println("launcher is $launcher") + launcher.launch(loader) + val simulation = loader.getDefault() + assertEquals(simulation.environment.nodeCount , 1) + simulation.play() + simulation.run() + val status = simulation.waitFor(Status.TERMINATED, 5, TimeUnit.MINUTES) + check(status == Status.TERMINATED) { + error("Simulation did not terminate after 5 minutes, status is $status") + } + // take the last created file at a specific path + // newestFileTime = max([os.path.getmtime(directory + '/' + file) for file in os.listdir(directory)], default=0.0) + val newestFile = File("src/test/resources/testNelderMeadResults") + .listFiles() + ?.maxByOrNull { it.lastModified() } + // from this file, i want to take all the values and relative keys of the object "variables" + val json = newestFile?.readText() + if(json != null) { + val element = Json.parseToJsonElement(json) + val variables = element.jsonObject["variables"]?.jsonObject + println("variables are $variables") + if (variables != null) { + val inputs = variables.toMap().mapValues { it.value.toString().toDouble() } + println("inputs are $inputs") + loader.launch(DefaultLauncher()) + val optimizedSimulation = loader.getWith(inputs) + val node = optimizedSimulation.environment.nodes[0] + val nodepos: Position<*> = optimizedSimulation.environment.getPosition(node) + println("node position is $nodepos") + nodepos.coordinates.forEachIndexed { index, value -> + assertEquals(0.0, value) + } + } + } + } + + @Test + fun `Nelder mead parameter optimization should TODO`() { +// val newestFile = File("src/test/resources/testNelderMeadResults") +// .listFiles() +// ?.maxByOrNull { it.lastModified() } +// // from this file, i want to take all the values and relative keys of the object "variables" +// val json = newestFile?.readText() +// if(json != null) { +// val element = Json.parseToJsonElement(json) +// val variables = element.jsonObject["variables"]?.jsonObject +// println("variables are $variables") +// if (variables != null) { +// val inputs = variables.toMap().mapValues { it.value.toString().toDouble() } +// println("inputs are $inputs") +// loader.launch(DefaultLauncher()) +// val customVars = mapOf("width" to 9.0, "height" to 9.0) +// val optimizedSimulation = loader.getWith(customVars) +// assertTrue(optimizedSimulation.environment.nodeCount in 75..85) +// } +// } + loader.launch(DefaultLauncher()) + val customVars = mapOf("zoom" to 0.001) + val optimizedSimulation = loader.getWith(customVars) + optimizedSimulation.play() + optimizedSimulation.run() + val node = optimizedSimulation.environment.nodes[0] + val nodepos: Position<*> = optimizedSimulation.environment.getPosition(node) + println("node position is $nodepos") + assertEquals(DoubleArray(2) { 2.2 }.toList(), nodepos.coordinates.toList()) + } +} + +/** + * The goal for the optimization. + */ +class Goal : (Environment) -> Double { + /** + * The target position of the node. + */ + val target = DoubleArray(2) { 2.2 } + + override fun invoke(env: Environment): Double { + val node: Node = env.nodes[0] + env.nodes.mapIndexed { idx, node -> env.getPosition(node) } + val nodePosition = env.getPosition(node).coordinates + return target.mapIndexed { index, value -> value - nodePosition[index] }.sum() + } +} + diff --git a/alchemist-loading/src/test/resources/images/base.png b/alchemist-loading/src/test/resources/images/base.png new file mode 100644 index 0000000000..82d78352c7 Binary files /dev/null and b/alchemist-loading/src/test/resources/images/base.png differ diff --git a/alchemist-loading/src/test/resources/images/square.png b/alchemist-loading/src/test/resources/images/square.png new file mode 100644 index 0000000000..317bf6d8b1 Binary files /dev/null and b/alchemist-loading/src/test/resources/images/square.png differ diff --git a/alchemist-loading/src/test/resources/testNelderMeadLauncher.yml b/alchemist-loading/src/test/resources/testNelderMeadLauncher.yml new file mode 100644 index 0000000000..326a70ac4c --- /dev/null +++ b/alchemist-loading/src/test/resources/testNelderMeadLauncher.yml @@ -0,0 +1,54 @@ +variables: + # goal: &goal + # formula: | + # it.unibo.alchemist.test.Goal() + # language: kotlin + zoom: &zoom + type: LinearVariable + parameters: [ 1, 0.001, 1, 0.01 ] + seed: &seed + min: 0 + max: 0 + step: 1 + default: 0 + +network-model: + type: ConnectWithinDistance + parameters: [ 5 ] + +seeds: + scenario: *seed + simulation: *seed + +incarnation: protelis + +environment: { type: ImageEnvironment, parameters: [images/base.png, 0.01, 0, 0] } + +_program-pools: + move: &move + - time-distribution: { type: ExponentialTime, parameters: [1] } + type: Event + actions: { type: FollowAtDistance, parameters: [destination, 0, 1] } + +deployments: + - type: Point + parameters: [0.1, 0.1] + programs: [*move] + contents: + - molecule: destination + concentration: [ 2, 2 ] + +terminate: + type: AfterTime + parameters: [10] + +#launcher: +# type: NelderMeadLauncher +# parameters: { +# outputPath: "src/test/resources/testNelderMeadResults", +# outputFileName: "testNelderMeadLauncher", +# objectiveFunction: *goal, +# variables: [ "zoom" ], +# seedName: "seed", +# repetitions: 1, +# } diff --git a/alchemist-loading/src/test/resources/testNelderMeadResults/testNelderMeadLauncher_2025-04-15T16-22-09.065959154.json b/alchemist-loading/src/test/resources/testNelderMeadResults/testNelderMeadLauncher_2025-04-15T16-22-09.065959154.json new file mode 100644 index 0000000000..bc14af0bf7 --- /dev/null +++ b/alchemist-loading/src/test/resources/testNelderMeadResults/testNelderMeadLauncher_2025-04-15T16-22-09.065959154.json @@ -0,0 +1 @@ +{"seed":1,"variables":{"width":2.5628155191630464,"height":17.182540214812793}} \ No newline at end of file diff --git a/alchemist-loading/src/test/resources/testNelderMeadResults/testNelderMeadLauncher_2025-05-07T17-11-57.151028509.json b/alchemist-loading/src/test/resources/testNelderMeadResults/testNelderMeadLauncher_2025-05-07T17-11-57.151028509.json new file mode 100644 index 0000000000..43fe80b1ec --- /dev/null +++ b/alchemist-loading/src/test/resources/testNelderMeadResults/testNelderMeadLauncher_2025-05-07T17-11-57.151028509.json @@ -0,0 +1 @@ +{"seed":1,"variables":{"zoom":0.009022468751106863}} \ No newline at end of file diff --git a/alchemist-loading/src/test/resources/testNelderMeadResults/testNelderMeadLauncher_2025-05-07T17-13-17.341174094.json b/alchemist-loading/src/test/resources/testNelderMeadResults/testNelderMeadLauncher_2025-05-07T17-13-17.341174094.json new file mode 100644 index 0000000000..43fe80b1ec --- /dev/null +++ b/alchemist-loading/src/test/resources/testNelderMeadResults/testNelderMeadLauncher_2025-05-07T17-13-17.341174094.json @@ -0,0 +1 @@ +{"seed":1,"variables":{"zoom":0.009022468751106863}} \ No newline at end of file diff --git a/alchemist-loading/src/test/resources/testNelderMeadResults/testNelderMeadLauncher_2025-05-07T17-16-06.717987460.json b/alchemist-loading/src/test/resources/testNelderMeadResults/testNelderMeadLauncher_2025-05-07T17-16-06.717987460.json new file mode 100644 index 0000000000..43fe80b1ec --- /dev/null +++ b/alchemist-loading/src/test/resources/testNelderMeadResults/testNelderMeadLauncher_2025-05-07T17-16-06.717987460.json @@ -0,0 +1 @@ +{"seed":1,"variables":{"zoom":0.009022468751106863}} \ No newline at end of file diff --git a/alchemist-loading/src/test/resources/testNelderMeadResults/testNelderMeadLauncher_2025-05-07T17-17-00.566773855.json b/alchemist-loading/src/test/resources/testNelderMeadResults/testNelderMeadLauncher_2025-05-07T17-17-00.566773855.json new file mode 100644 index 0000000000..43fe80b1ec --- /dev/null +++ b/alchemist-loading/src/test/resources/testNelderMeadResults/testNelderMeadLauncher_2025-05-07T17-17-00.566773855.json @@ -0,0 +1 @@ +{"seed":1,"variables":{"zoom":0.009022468751106863}} \ No newline at end of file diff --git a/alchemist-loading/src/test/resources/testNelderMeadResults/testNelderMeadLauncher_2025-05-07T17-19-49.731927730.json b/alchemist-loading/src/test/resources/testNelderMeadResults/testNelderMeadLauncher_2025-05-07T17-19-49.731927730.json new file mode 100644 index 0000000000..43fe80b1ec --- /dev/null +++ b/alchemist-loading/src/test/resources/testNelderMeadResults/testNelderMeadLauncher_2025-05-07T17-19-49.731927730.json @@ -0,0 +1 @@ +{"seed":1,"variables":{"zoom":0.009022468751106863}} \ No newline at end of file diff --git a/alchemist-loading/src/test/resources/testNelderMeadResults/testNelderMeadLauncher_2025-05-07T17-22-30.509375333.json b/alchemist-loading/src/test/resources/testNelderMeadResults/testNelderMeadLauncher_2025-05-07T17-22-30.509375333.json new file mode 100644 index 0000000000..43fe80b1ec --- /dev/null +++ b/alchemist-loading/src/test/resources/testNelderMeadResults/testNelderMeadLauncher_2025-05-07T17-22-30.509375333.json @@ -0,0 +1 @@ +{"seed":1,"variables":{"zoom":0.009022468751106863}} \ No newline at end of file diff --git a/alchemist-loading/src/test/resources/testNelderMeadResults/testNelderMeadLauncher_2025-05-07T17-32-06.814812034.json b/alchemist-loading/src/test/resources/testNelderMeadResults/testNelderMeadLauncher_2025-05-07T17-32-06.814812034.json new file mode 100644 index 0000000000..43fe80b1ec --- /dev/null +++ b/alchemist-loading/src/test/resources/testNelderMeadResults/testNelderMeadLauncher_2025-05-07T17-32-06.814812034.json @@ -0,0 +1 @@ +{"seed":1,"variables":{"zoom":0.009022468751106863}} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1ef6acce7f..12ac2d99f5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -112,6 +112,7 @@ protelis-interpreter = { module = "org.protelis:protelis-interpreter", version.r protelis-lang = { module = "org.protelis:protelis-lang", version.ref = "protelis" } quadtree = "org.danilopianini:java-quadtree:1.0.1" resourceloader = "org.danilopianini:thread-inheritable-resource-loader:0.3.7" +rrmxmx = { module = "org.danilopianini:rrmxmx-kt", version = "2.0.4" } rtree = "com.github.davidmoten:rtree:0.12" scafi-core = { module = "it.unibo.scafi:scafi-core_2.13", version.ref = "scafi" } scala-compiler = { module = "org.scala-lang:scala-compiler", version.ref = "scala" }