All Downloads are FREE. Search and download functionalities are using the official Maven repository.

it.unibo.alchemist.CollektiveIncarnation.kt Maven / Gradle / Ivy

There is a newer version: 11.1.3
Show newest version
package it.unibo.alchemist

import com.github.benmanes.caffeine.cache.Caffeine
import com.github.benmanes.caffeine.cache.LoadingCache
import it.unibo.alchemist.actions.RunCollektiveProgram
import it.unibo.alchemist.collektive.device.CollektiveDevice
import it.unibo.alchemist.model.Action
import it.unibo.alchemist.model.Actionable
import it.unibo.alchemist.model.Condition
import it.unibo.alchemist.model.Context
import it.unibo.alchemist.model.Environment
import it.unibo.alchemist.model.Incarnation
import it.unibo.alchemist.model.Molecule
import it.unibo.alchemist.model.Node
import it.unibo.alchemist.model.Position
import it.unibo.alchemist.model.Reaction
import it.unibo.alchemist.model.TimeDistribution
import it.unibo.alchemist.model.conditions.AbstractCondition
import it.unibo.alchemist.model.molecules.SimpleMolecule
import it.unibo.alchemist.model.nodes.GenericNode
import it.unibo.alchemist.model.reactions.Event
import it.unibo.alchemist.model.timedistributions.DiracComb
import it.unibo.alchemist.model.times.DoubleTime
import it.unibo.alchemist.util.RandomGenerators.nextDouble
import it.unibo.collektive.aggregate.api.Aggregate
import it.unibo.collektive.compiler.CollektiveJVMCompiler
import it.unibo.collektive.compiler.logging.CollectingMessageCollector
import it.unibo.collektive.compiler.util.md5
import it.unibo.collektive.compiler.util.toBase32
import org.apache.commons.math3.random.RandomGenerator
import org.danilopianini.util.ListSet
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity.ERROR
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity.EXCEPTION
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity.INFO
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity.LOGGING
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity.OUTPUT
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity.STRONG_WARNING
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity.WARNING
import org.jetbrains.kotlin.codegen.state.GenerationState
import org.slf4j.LoggerFactory
import java.io.File
import java.lang.reflect.Method
import java.net.URLClassLoader
import javax.script.ScriptEngineManager
import kotlin.io.path.createTempDirectory
import kotlin.reflect.KProperty
import kotlin.reflect.full.starProjectedType

/**
 * Collektive incarnation in Alchemist.
 */
class CollektiveIncarnation

: Incarnation where P : Position

{ override fun getProperty(node: Node, molecule: Molecule, property: String?): Double { val interpreted = when (property.isNullOrBlank()) { true -> node.getConcentration(molecule) else -> { val concentration = node.getConcentration(molecule) val concentrationType = when (concentration) { null -> "Any?" else -> { val type = concentration::class.starProjectedType "$type${"?".takeIf { type.isMarkedNullable }.orEmpty()}" } } val toInvoke = propertyCache.get( "import kotlin.math.*; val x: ($concentrationType) -> Any? = { $property }; x", ) toInvoke(concentration) } } return when (interpreted) { is Double -> interpreted is Number -> interpreted.toDouble() is String -> interpreted.toDoubleOrNull() ?: Double.NaN else -> Double.NaN } } override fun createMolecule(molecule: String) = SimpleMolecule(molecule) override fun createConcentration(concentration: Any?): Any? = concentration override fun createConcentration() = Unit override fun createAction( randomGenerator: RandomGenerator, environment: Environment, node: Node?, time: TimeDistribution, actionable: Actionable, additionalParameters: Any?, ): Action { requireNotNull(node) { "Collektive requires a device and cannot execute in a Global Reaction" } if (additionalParameters is CharSequence) { return RunCollektiveProgram(node, additionalParameters.toString()) } val parameters = additionalParameters as? Map<*, *> requireNotNull(parameters) { val type = additionalParameters?.let { it::class.simpleName } ?: "null" "Invalid parameters for Collektive. Map required, but $type has been provided: $additionalParameters" } val entrypoint: String = requireNotNull(parameters["entrypoint"]) { "No entrypoint provided in $additionalParameters" }.toString() val code: String = parameters["code"]?.toString().orEmpty() val sourceSets: List = parameters["source-sets"].toFiles() val classpath = sourceSets.joinToString(separator = File.pathSeparator) { it.absolutePath } val internalIdentifier = "$classpath$code$entrypoint".md5().toBase32() val (name: String, methodName: String) = when (val nameFromParameters = parameters["name"]) { null -> "collektive$internalIdentifier".let { it to it } else -> nameFromParameters.toString().let { originalName -> originalName to "${originalName.replaceFirstChar { it.lowercase() }}$internalIdentifier" } } check(name.matches(validName)) { "Invalid name for Collektive program: $name" } val className = methodName.replaceFirstChar { it.uppercase() } val packageMatch = findPackage.find(code) val packageName = packageMatch?.groupValues?.get(1).orEmpty() val classFqName = classFqNameFrom(packageName, className) val classLoader = classLoaders.get(methodName) fun loadMethod() = classLoader.loadClass(classFqName).methods.first { it.name == methodName } val methodToCall: Method = runCatching { loadMethod() }.recover { exception -> logger.info("Collektive program $name not found, compiling", exception) compileCollektive(name, sourceSets, className, methodName, code, entrypoint, classLoader) loadMethod() }.getOrThrow() return RunCollektiveProgram(node, methodToCall, name) } override fun createCondition( randomGenerator: RandomGenerator, environment: Environment?, node: Node?, time: TimeDistribution, actionable: Actionable, additionalParameters: Any?, ): Condition = object : AbstractCondition(requireNotNull(node)) { override fun getContext() = Context.LOCAL override fun getPropensityContribution(): Double = 1.0 override fun isValid(): Boolean = true } override fun createReaction( randomGenerator: RandomGenerator, environment: Environment, node: Node, timeDistribution: TimeDistribution, parameter: Any?, ): Reaction = Event(node, timeDistribution).also { it.actions = ListSet.of( createAction(randomGenerator, environment, node, timeDistribution, it, parameter), ) } override fun createTimeDistribution( randomGenerator: RandomGenerator, environment: Environment, node: Node?, parameter: Any?, ): TimeDistribution { val frequency = when (parameter) { null -> 1.0 is Number -> parameter.toDouble() is String -> parameter.toDouble() else -> error("Invalid time distribution parameter: $parameter") } return DiracComb(DoubleTime(randomGenerator.nextDouble(0.0, 1.0 / frequency)), frequency) } override fun createNode( randomGenerator: RandomGenerator, environment: Environment, parameter: Any?, ): Node = GenericNode(environment).also { genericNode -> genericNode.addProperty( CollektiveDevice( environment, genericNode, when (parameter) { null -> null is Number -> DoubleTime(parameter.toDouble()) is String -> DoubleTime(parameter.toDouble()) else -> error("Invalid message retention time: $parameter") }, ), ) } private companion object { private object ScriptEngine { operator fun getValue(thisRef: Any?, property: KProperty<*>) = ScriptEngineManager().getEngineByName(property.name) ?: error("No script engine with ${property.name} found.") } private val findPackage = Regex("""package\s+((\w+\.)*\w+)(\s|;|/|$)""", RegexOption.MULTILINE) private val validName = Regex("^[a-zA-Z_][a-zA-Z0-9_]*$", RegexOption.MULTILINE) private val logger = LoggerFactory.getLogger(CollektiveIncarnation::class.java) private val kotlin by ScriptEngine private val defaultLambda: (Any?) -> Any? = { Double.NaN } private val propertyCache: LoadingCache Any?> = Caffeine .newBuilder() .maximumSize(1000) .build { property -> runCatching { @Suppress("UNCHECKED_CAST") when (val interpreted = kotlin.eval(property)) { is (Nothing) -> Any? -> interpreted else -> defaultLambda } as (Any?) -> Any? }.getOrElse { defaultLambda } } private val classLoaders: LoadingCache = Caffeine.newBuilder().maximumSize(1000).build { name -> val outputFolder = createTempDirectory(name).toFile() URLClassLoader(arrayOf(outputFolder.toURI().toURL()), Thread.currentThread().contextClassLoader) } private fun classFqNameFrom(packageName: String?, className: String) = "${packageName?.takeUnless { it.isEmpty() }?.let { "$it." }.orEmpty() }$className" private fun compileCollektive( name: String, sourceSets: List, className: String, methodName: String, code: String, entrypoint: String, classLoader: URLClassLoader, ): Pair { val inputFolder = createTempDirectory("collektive").toFile() logger.info("Compiling Collektive program {} in folder {}", name, inputFolder.absolutePath) val finalCode = """ |@file:JvmName("$className") |${code.replace("\n", "\n|")} |context(${CollektiveDevice::class.qualifiedName}

) |fun

> ${Aggregate::class.qualifiedName}.$methodName() = | $entrypoint """.trimMargin() logger.info("Final code for Collektive program {}:\n{}", name, finalCode) inputFolder.resolve("$className.kt").writeText(finalCode) val outputFolder = File(classLoader.urLs.first().file) check(outputFolder.exists() && outputFolder.isDirectory) { "Output folder ${outputFolder.absolutePath} does not exist or is not a directory" } val messages = CollectingMessageCollector() val result: GenerationState? = CollektiveJVMCompiler.compile( sourceSets + inputFolder, moduleName = name, outputFolder = outputFolder, messageCollector = messages, ) messages.messages.forEach { (severity, message) -> when (severity) { ERROR, EXCEPTION -> logger.error(message) STRONG_WARNING, WARNING -> logger.error(message) INFO, OUTPUT -> logger.info(message) LOGGING -> logger.debug(message) } } check(!messages.hasErrors() && result != null) { "Compilation of Collektive program $name failed:\n$finalCode" } return messages to result } /** * Convert the source set to a list of files. */ private fun Any?.toFiles(): List = when (this) { null -> emptyList() is File -> listOf(this) is CharSequence -> { val file = File(toString()) check(file.exists()) { "Collektive source root ${file.absolutePath} does not exist" } listOf(file) } is Iterable<*> -> flatMap { files -> files.toFiles() } else -> error("Invalid source set of type ${this::class.simpleName ?: "anonymous"}: $this") } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy