diff --git a/cli/src/main/resources/META-INF/lcaac.properties b/cli/src/main/resources/META-INF/lcaac.properties index 15331f14..354ef0ac 100644 --- a/cli/src/main/resources/META-INF/lcaac.properties +++ b/cli/src/main/resources/META-INF/lcaac.properties @@ -1,4 +1,4 @@ -#Thu Jul 24 10:18:57 CEST 2025 +#Thu Aug 14 10:41:08 CEST 2025 author=Kleis Technology description=LCA as Code CLI version=1.7.13 diff --git a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/Evaluator.kt b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/Evaluator.kt index 7b62efc0..d0fed351 100644 --- a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/Evaluator.kt +++ b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/Evaluator.kt @@ -6,12 +6,13 @@ import ch.kleis.lcaac.core.lang.evaluator.protocol.CachedOracle import ch.kleis.lcaac.core.lang.evaluator.protocol.Learner import ch.kleis.lcaac.core.lang.expression.* import ch.kleis.lcaac.core.lang.register.ProcessKey +import ch.kleis.lcaac.core.math.Operations import ch.kleis.lcaac.core.math.QuantityOperations import org.slf4j.LoggerFactory -class Evaluator( +class Evaluator( private val symbolTable: SymbolTable, - private val ops: QuantityOperations, + private val ops: Operations, private val sourceOps: DataSourceOperations, ) { @Suppress("PrivatePropertyName") @@ -35,7 +36,7 @@ class Evaluator( } } - fun with(template: EProcessTemplate): Evaluator { + fun with(template: EProcessTemplate): Evaluator { val processKey = ProcessKey(template.body.name) if (symbolTable.processTemplates[processKey] != null) throw IllegalStateException("Process ${template.body.name} already exists") val st = this.symbolTable.copy( diff --git a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/protocol/Oracle.kt b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/protocol/Oracle.kt index 88f7ca2b..a8d1568e 100644 --- a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/protocol/Oracle.kt +++ b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/protocol/Oracle.kt @@ -5,16 +5,18 @@ import ch.kleis.lcaac.core.lang.SymbolTable import ch.kleis.lcaac.core.lang.evaluator.step.CompleteTerminals import ch.kleis.lcaac.core.lang.evaluator.step.Reduce import ch.kleis.lcaac.core.lang.expression.EProcess -import ch.kleis.lcaac.core.lang.expression.EProcessTemplateApplication -import ch.kleis.lcaac.core.lang.resolver.ProcessResolver +import ch.kleis.lcaac.core.lang.expression.ProcessAnnotation +import ch.kleis.lcaac.core.lang.resolver.BareProcessResolver +import ch.kleis.lcaac.core.lang.resolver.CachedProcessResolver +import ch.kleis.lcaac.core.lang.resolver.ProcessTemplateResolver import ch.kleis.lcaac.core.lang.resolver.SubstanceCharacterizationResolver -import ch.kleis.lcaac.core.math.QuantityOperations +import ch.kleis.lcaac.core.math.Operations import com.mayakapps.kache.InMemoryKache import com.mayakapps.kache.KacheStrategy import com.mayakapps.kache.ObjectKache import kotlinx.coroutines.runBlocking -interface Oracle { +interface Oracle { fun answer(ports: Set>): Set> { return ports.mapNotNull { answerRequest(it) @@ -23,15 +25,15 @@ interface Oracle { fun answerRequest(request: Request): Response? } -class CachedOracle( - private val inner: Oracle, +class CachedOracle( + private val inner: Oracle, private val cache: ObjectKache, Response> = InMemoryKache(maxSize = 1024) { strategy = KacheStrategy.LRU } -) : Oracle { +) : Oracle { constructor( symbolTable: SymbolTable, - ops: QuantityOperations, + ops: Operations, sourceOps: DataSourceOperations, cache: ObjectKache, Response> = InMemoryKache(maxSize = 1024) { strategy = KacheStrategy.LRU @@ -50,14 +52,14 @@ class CachedOracle( } } -class BareOracle( +class BareOracle( val symbolTable: SymbolTable, - val ops: QuantityOperations, - sourceOps: DataSourceOperations, -): Oracle { + val ops: Operations, + val sourceOps: DataSourceOperations, +): Oracle { private val reduceDataExpressions = Reduce(symbolTable, ops, sourceOps) private val completeTerminals = CompleteTerminals(ops) - private val processResolver = ProcessResolver(symbolTable) + private val processTemplateResolver = ProcessTemplateResolver(symbolTable) private val substanceCharacterizationResolver = SubstanceCharacterizationResolver(symbolTable) override fun answerRequest(request: Request): Response? { @@ -67,15 +69,19 @@ class BareOracle( } } + private fun answerProductRequest(request: ProductRequest): ProductResponse? { val spec = request.value - val template = processResolver.resolve(spec) ?: return null - val arguments = template.params - .plus(spec.fromProcess?.arguments ?: emptyMap()) - val expression = EProcessTemplateApplication(template, arguments) - val process = expression - .let(reduceDataExpressions::apply) - .let(completeTerminals::apply) + val template = processTemplateResolver.resolve(spec) ?: return null + + val processResolver = if (template.annotations.contains(ProcessAnnotation.CACHED)) { + CachedProcessResolver(symbolTable, ops, sourceOps) + } else { + BareProcessResolver(symbolTable, ops, sourceOps) + } + + val process = processResolver.resolve(template, spec) + val selectedPortIndex = indexOf(request.value.name, process) return ProductResponse(request.address, process, selectedPortIndex) } @@ -92,5 +98,4 @@ class BareOracle( private fun indexOf(productName: String, process: EProcess): Int { return process.products.indexOfFirst { it.product.name == productName } } - -} +} \ No newline at end of file diff --git a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/expression/EMapper.kt b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/expression/EMapper.kt new file mode 100644 index 00000000..58f4ceff --- /dev/null +++ b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/expression/EMapper.kt @@ -0,0 +1,70 @@ +package ch.kleis.lcaac.core.lang.expression + +import ch.kleis.lcaac.core.lang.value.* + +object EMapper { + fun toDataExpression(value: DataValue): DataExpression { + return when (value) { + is QuantityValue -> value.toEQuantityScale() + is RecordValue -> value.toERecord() + is StringValue -> value.toEStringLiteral() + } + } + + fun toFromProcess(value: FromProcessRefValue): FromProcess { + val labels = MatchLabels(value.matchLabels.map { it.key to it.value.toEStringLiteral() }.toMap()) + val arguments: Map> = value.arguments.map { it.key to toDataExpression(it.value) }.toMap() + return FromProcess(value.name, labels, arguments) + } + + fun toETechnoExchange(quantity: QuantityValue, product: ProductValue): ETechnoExchange { + return ETechnoExchange( + quantity = quantity.toEQuantityScale(), + product = EProductSpec( + product.name, + product.referenceUnit.toEUnitLiteral(), + product.fromProcessRef?.let { toFromProcess(it) } + ) + ) + } + + fun toETechnoExchange(value: TechnoExchangeValue): ETechnoExchange { + return ETechnoExchange( + quantity = value.quantity.toEQuantityScale(), + product = EProductSpec( + value.product.name, + value.product.referenceUnit.toEUnitLiteral(), + value.product.fromProcessRef?.let { toFromProcess(it) } + ), + allocation = value.allocation?.toEQuantityScale() + ) + } + + fun toEBioExchange(quantity: QuantityValue, substance: SubstanceValue): EBioExchange { + return EBioExchange( + quantity = quantity.toEQuantityScale(), + substance = when (substance) { + is FullyQualifiedSubstanceValue -> ESubstanceSpec( + name = substance.getShortName(), + displayName = substance.getDisplayName(), + type = substance.type, + compartment = substance.compartment, + subCompartment = substance.subcompartment, + referenceUnit = substance.referenceUnit.toEUnitLiteral() + ) + is PartiallyQualifiedSubstanceValue -> ESubstanceSpec( + name = substance.getShortName(), + displayName = substance.getDisplayName(), + referenceUnit = substance.referenceUnit.toEUnitLiteral() + ) + } + ) + } + + fun toEImpact(quantity: QuantityValue, value: IndicatorValue): EImpact { + return EImpact( + quantity = quantity.toEQuantityScale(), + indicator = EIndicatorSpec(value.name, value.referenceUnit.toEUnitLiteral()) + ) + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/expression/ProcessTemplateExpression.kt b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/expression/ProcessTemplateExpression.kt index 341dfffb..3174a9fd 100644 --- a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/expression/ProcessTemplateExpression.kt +++ b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/expression/ProcessTemplateExpression.kt @@ -8,11 +8,16 @@ sealed interface ProcessTemplateExpression { companion object } +enum class ProcessAnnotation { + CACHED +} + @optics data class EProcessTemplate( val params: Map> = emptyMap(), val locals: Map> = emptyMap(), val body: EProcess, + val annotations: Set = emptySet(), ) : ProcessTemplateExpression { companion object } diff --git a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/resolver/ProcessResolver.kt b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/resolver/ProcessResolver.kt index 83de0ec5..ac0c3d28 100644 --- a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/resolver/ProcessResolver.kt +++ b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/resolver/ProcessResolver.kt @@ -1,38 +1,97 @@ package ch.kleis.lcaac.core.lang.resolver +import ch.kleis.lcaac.core.assessment.AnalysisProgram +import ch.kleis.lcaac.core.datasource.DataSourceOperations import ch.kleis.lcaac.core.lang.SymbolTable -import ch.kleis.lcaac.core.lang.evaluator.EvaluatorException -import ch.kleis.lcaac.core.lang.expression.EProcessTemplate -import ch.kleis.lcaac.core.lang.expression.EProductSpec -import ch.kleis.lcaac.core.lang.expression.EStringLiteral - -class ProcessResolver( - private val symbolTable: SymbolTable -) { - fun resolve(spec: EProductSpec): EProcessTemplate? { - if (spec.fromProcess == null) { - val matches = symbolTable.getAllTemplatesByProductName(spec.name) - return when (matches.size) { - 0 -> null - 1 -> matches.first() - else -> throw EvaluatorException("more than one processes found providing ${spec.name}") - } +import ch.kleis.lcaac.core.lang.evaluator.EvaluationTrace +import ch.kleis.lcaac.core.lang.evaluator.Evaluator +import ch.kleis.lcaac.core.lang.evaluator.step.CompleteTerminals +import ch.kleis.lcaac.core.lang.evaluator.step.Reduce +import ch.kleis.lcaac.core.lang.expression.* +import ch.kleis.lcaac.core.lang.register.ProcessKey +import ch.kleis.lcaac.core.lang.value.MatrixColumnIndex +import ch.kleis.lcaac.core.lang.value.QuantityValue +import ch.kleis.lcaac.core.lang.value.QuantityValueOperations +import ch.kleis.lcaac.core.lang.value.TechnoExchangeValue +import ch.kleis.lcaac.core.math.Operations +import ch.kleis.lcaac.core.matrix.ImpactFactorMatrix + +interface ProcessResolver { + fun resolve(template: EProcessTemplate, spec: EProductSpec): EProcess +} + +class CachedProcessResolver( + val symbolTable: SymbolTable, + val ops: Operations, + val sourceOps: DataSourceOperations, +) : ProcessResolver { + override fun resolve(template: EProcessTemplate, spec: EProductSpec): EProcess { + val trace = getTrace(template, spec) + val entryPoint = trace.getEntryPoint() + val analysis = AnalysisProgram(trace.getSystemValue(), entryPoint, ops).run() + val inputQuantity = inputQuantityAnalysis(entryPoint.products, analysis.impactFactors) + + val inputs = analysis.impactFactors.getInputProducts().map { + EMapper.toETechnoExchange(inputQuantity(it), it) } - val name = spec.fromProcess.name - val labels = spec.fromProcess.matchLabels.elements.mapValues { - when (val v = it.value) { - is EStringLiteral -> v.value - else -> throw EvaluatorException("$v is not a valid label value") - } - } - return symbolTable.getTemplate(name, labels)?.let { candidate -> - val providedProducts = candidate.body.products.map { it.product.name } - if (!providedProducts.contains(spec.name)) { - val s = if (labels.isEmpty()) name else "$name$labels" - throw EvaluatorException("no process '$s' providing '${spec.name}' found") + val biosphere = analysis.impactFactors.getSubstances().map { + EMapper.toEBioExchange(inputQuantity(it), it) + } + + val impacts = analysis.impactFactors.getIndicators().map { + EMapper.toEImpact(inputQuantity(it), it) + } + + return template.body.copy( + products = entryPoint.products.map { EMapper.toETechnoExchange(it)}, + inputs = inputs.map { ETechnoBlockEntry(it) }, + biosphere = biosphere.map { EBioBlockEntry(it) }, + impacts = impacts.map { EImpactBlockEntry(it)} + ) + } + + private fun getTrace(template: EProcessTemplate, spec: EProductSpec): EvaluationTrace { + val arguments = template.params.plus(spec.fromProcess?.arguments ?: emptyMap()) + val newAnnotations = template.annotations.filter { it != ProcessAnnotation.CACHED }.toSet() + val newTemplate = template.copy(annotations = newAnnotations) + + val newSymbolTable = symbolTable.copy(processTemplates = symbolTable.processTemplates.override( + ProcessKey(newTemplate.body.name, newTemplate.body.labels.mapValues { it.value.value }), + newTemplate + )) + val evaluator = Evaluator(newSymbolTable, ops, sourceOps) + + return evaluator.trace(newTemplate, arguments) + } + + private fun inputQuantityAnalysis( + products: List>, + impactFactors: ImpactFactorMatrix + ): (MatrixColumnIndex) -> QuantityValue { + return { inputPort: MatrixColumnIndex -> + with(QuantityValueOperations(ops)) { + products + .map { impactFactors.characterizationFactor(it.port(), inputPort) * it.quantity } + .reduce { a, b -> a + b } } - candidate } } } + +class BareProcessResolver( + val symbolTable: SymbolTable, + val ops: Operations, + val sourceOps: DataSourceOperations, +) : ProcessResolver { + private val reduceDataExpressions = Reduce(symbolTable, ops, sourceOps) + private val completeTerminals = CompleteTerminals(ops) + + override fun resolve(template: EProcessTemplate, spec: EProductSpec): EProcess { + val arguments = template.params.plus(spec.fromProcess?.arguments ?: emptyMap()) + val expression = EProcessTemplateApplication(template, arguments) + return expression + .let(reduceDataExpressions::apply) + .let(completeTerminals::apply) + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/resolver/ProcessTemplateResolver.kt b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/resolver/ProcessTemplateResolver.kt new file mode 100644 index 00000000..6d9d3eca --- /dev/null +++ b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/resolver/ProcessTemplateResolver.kt @@ -0,0 +1,38 @@ +package ch.kleis.lcaac.core.lang.resolver + +import ch.kleis.lcaac.core.lang.SymbolTable +import ch.kleis.lcaac.core.lang.evaluator.EvaluatorException +import ch.kleis.lcaac.core.lang.expression.EProcessTemplate +import ch.kleis.lcaac.core.lang.expression.EProductSpec +import ch.kleis.lcaac.core.lang.expression.EStringLiteral + +class ProcessTemplateResolver( + private val symbolTable: SymbolTable +) { + fun resolve(spec: EProductSpec): EProcessTemplate? { + if (spec.fromProcess == null) { + val matches = symbolTable.getAllTemplatesByProductName(spec.name) + return when (matches.size) { + 0 -> null + 1 -> matches.first() + else -> throw EvaluatorException("more than one processes found providing ${spec.name}") + } + } + + val name = spec.fromProcess.name + val labels = spec.fromProcess.matchLabels.elements.mapValues { + when (val v = it.value) { + is EStringLiteral -> v.value + else -> throw EvaluatorException("$v is not a valid label value") + } + } + return symbolTable.getTemplate(name, labels)?.let { candidate -> + val providedProducts = candidate.body.products.map { it.product.name } + if (!providedProducts.contains(spec.name)) { + val s = if (labels.isEmpty()) name else "$name$labels" + throw EvaluatorException("no process '$s' providing '${spec.name}' found") + } + candidate + } + } +} diff --git a/core/src/main/kotlin/ch/kleis/lcaac/core/matrix/ImpactFactorMatrix.kt b/core/src/main/kotlin/ch/kleis/lcaac/core/matrix/ImpactFactorMatrix.kt index c7afcec6..799b1756 100644 --- a/core/src/main/kotlin/ch/kleis/lcaac/core/matrix/ImpactFactorMatrix.kt +++ b/core/src/main/kotlin/ch/kleis/lcaac/core/matrix/ImpactFactorMatrix.kt @@ -1,8 +1,11 @@ package ch.kleis.lcaac.core.matrix +import ch.kleis.lcaac.core.lang.value.IndicatorValue import ch.kleis.lcaac.core.lang.value.MatrixColumnIndex +import ch.kleis.lcaac.core.lang.value.ProductValue import ch.kleis.lcaac.core.lang.value.QuantityValue import ch.kleis.lcaac.core.lang.value.QuantityValueOperations +import ch.kleis.lcaac.core.lang.value.SubstanceValue import ch.kleis.lcaac.core.lang.value.UnitValue import ch.kleis.lcaac.core.math.Operations @@ -57,5 +60,15 @@ class ImpactFactorMatrix( return observablePorts.size() * controllablePorts.size() } + fun getIndicators(): List> { + return controllablePorts.getElements().filter { it is IndicatorValue }.map { it as IndicatorValue } + } + + fun getSubstances(): List> { + return controllablePorts.getElements().filter { it is SubstanceValue }.map { it as SubstanceValue } + } + fun getInputProducts(): List> { + return controllablePorts.getElements().filter { it is ProductValue }.map { it as ProductValue } + } } diff --git a/core/src/main/kotlin/ch/kleis/lcaac/core/testing/BasicTestRunner.kt b/core/src/main/kotlin/ch/kleis/lcaac/core/testing/BasicTestRunner.kt index 2a2565f5..b07959e0 100644 --- a/core/src/main/kotlin/ch/kleis/lcaac/core/testing/BasicTestRunner.kt +++ b/core/src/main/kotlin/ch/kleis/lcaac/core/testing/BasicTestRunner.kt @@ -10,13 +10,14 @@ import ch.kleis.lcaac.core.lang.evaluator.reducer.DataExpressionReducer import ch.kleis.lcaac.core.lang.value.DataValue import ch.kleis.lcaac.core.lang.value.QuantityValue import ch.kleis.lcaac.core.lang.value.QuantityValueOperations +import ch.kleis.lcaac.core.math.basic.BasicMatrix import ch.kleis.lcaac.core.math.basic.BasicNumber import ch.kleis.lcaac.core.math.basic.BasicOperations class BasicTestRunner( symbolTable: SymbolTable, sourceOps: DataSourceOperations, - private val evaluator: Evaluator = Evaluator(symbolTable, BasicOperations, sourceOps), + private val evaluator: Evaluator = Evaluator(symbolTable, BasicOperations, sourceOps), private val dataReducer: DataExpressionReducer = DataExpressionReducer( symbolTable.data, symbolTable.dataSources, diff --git a/core/src/test/kotlin/ch/kleis/lcaac/core/lang/evaluator/protocol/OracleTest.kt b/core/src/test/kotlin/ch/kleis/lcaac/core/lang/evaluator/protocol/OracleTest.kt index 16e94d91..0eb38923 100644 --- a/core/src/test/kotlin/ch/kleis/lcaac/core/lang/evaluator/protocol/OracleTest.kt +++ b/core/src/test/kotlin/ch/kleis/lcaac/core/lang/evaluator/protocol/OracleTest.kt @@ -5,20 +5,26 @@ import ch.kleis.lcaac.core.lang.expression.EProcess import ch.kleis.lcaac.core.lang.expression.EProcessTemplate import ch.kleis.lcaac.core.lang.expression.EProductSpec import ch.kleis.lcaac.core.lang.expression.ETechnoExchange +import ch.kleis.lcaac.core.lang.expression.ProcessAnnotation.CACHED import ch.kleis.lcaac.core.lang.fixture.ImpactBlockFixture import ch.kleis.lcaac.core.lang.fixture.ProductFixture import ch.kleis.lcaac.core.lang.fixture.QuantityFixture import ch.kleis.lcaac.core.lang.register.ProcessKey import ch.kleis.lcaac.core.lang.register.ProcessTemplateRegister +import ch.kleis.lcaac.core.lang.resolver.BareProcessResolver +import ch.kleis.lcaac.core.lang.resolver.CachedProcessResolver import ch.kleis.lcaac.core.math.basic.BasicNumber import ch.kleis.lcaac.core.math.basic.BasicOperations +import io.mockk.every import io.mockk.mockk +import io.mockk.mockkConstructor import io.mockk.spyk import io.mockk.verify +import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test +import kotlin.test.assertTrue class OracleTest { - @Test fun cachedOracle() { // given @@ -57,4 +63,87 @@ class OracleTest { inner.answerRequest(any()) } } + + @Nested + inner class AnswerProductRequest { + private val spec = EProductSpec("carrot") + private val request = ProductRequest(Address(0, 0), spec) + private val mockProcess = mockk>(relaxed = true) + + @Test + fun `should use CachedProcessResolver when template has CACHED annotation`() { + // given + val template = EProcessTemplate( + body = EProcess( + name = "eProcess", + products = listOf( + ETechnoExchange(QuantityFixture.oneKilogram, ProductFixture.carrot) + ), + impacts = listOf( + ImpactBlockFixture.oneClimateChange + ), + ), + annotations = setOf(CACHED) + ) + val symbolTable = SymbolTable( + processTemplates = ProcessTemplateRegister.from(mapOf( + ProcessKey("eProcess") to template + )) + ) + val oracle = spyk(BareOracle(symbolTable, BasicOperations, mockk())) + + mockkConstructor(CachedProcessResolver::class) + every { + anyConstructed>().resolve(template, spec) + } returns mockProcess + + // when + val result = oracle.answerRequest(request) + + // then + assertTrue(result is ProductResponse) + verify { + anyConstructed>() + .resolve(template, spec) + } + } + + @Test + fun `should use BareProcessResolver when template has no CACHED annotation`() { + // given + val template = EProcessTemplate( + body = EProcess( + name = "eProcess", + products = listOf( + ETechnoExchange(QuantityFixture.oneKilogram, ProductFixture.carrot) + ), + impacts = listOf( + ImpactBlockFixture.oneClimateChange + ), + ), + annotations = setOf() + ) + val symbolTable = SymbolTable( + processTemplates = ProcessTemplateRegister.from(mapOf( + ProcessKey("eProcess") to template + )) + ) + val oracle = spyk(BareOracle(symbolTable, BasicOperations, mockk())) + + mockkConstructor(BareProcessResolver::class) + every { + anyConstructed>().resolve(template, spec) + } returns mockProcess + + // when + val result = oracle.answerRequest(request) + + // then + assertTrue(result is ProductResponse) + verify { + anyConstructed>() + .resolve(template, spec) + } + } + } } diff --git a/core/src/test/kotlin/ch/kleis/lcaac/core/lang/expression/EMapperTest.kt b/core/src/test/kotlin/ch/kleis/lcaac/core/lang/expression/EMapperTest.kt new file mode 100644 index 00000000..bf6d9738 --- /dev/null +++ b/core/src/test/kotlin/ch/kleis/lcaac/core/lang/expression/EMapperTest.kt @@ -0,0 +1,190 @@ +package ch.kleis.lcaac.core.lang.expression + +import ch.kleis.lcaac.core.lang.fixture.UnitValueFixture +import ch.kleis.lcaac.core.lang.value.FromProcessRefValue +import ch.kleis.lcaac.core.lang.value.FullyQualifiedSubstanceValue +import ch.kleis.lcaac.core.lang.value.IndicatorValue +import ch.kleis.lcaac.core.lang.value.PartiallyQualifiedSubstanceValue +import ch.kleis.lcaac.core.lang.value.ProductValue +import ch.kleis.lcaac.core.lang.value.QuantityValue +import ch.kleis.lcaac.core.lang.value.RecordValue +import ch.kleis.lcaac.core.lang.value.StringValue +import ch.kleis.lcaac.core.lang.value.TechnoExchangeValue +import ch.kleis.lcaac.core.math.basic.BasicNumber +import io.mockk.InternalPlatformDsl.toStr +import org.junit.jupiter.api.Nested +import kotlin.collections.mapOf +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.to + +class EMapperTest { + private val sut = EMapper + + @Nested + inner class ToDataExpression { + @Test + fun `when QuantityValue should map to EQuantityScale`() { + // Given + val value = QuantityValue(BasicNumber(1.0), UnitValueFixture.kg()) + + // When + val actual = sut.toDataExpression(value) + + // Then + assertEquals(value.toEQuantityScale(), actual) + } + + @Test + fun `when RecordValue should map to ERecord`() { + // Given + val value = RecordValue(mapOf( + "quantity" to QuantityValue(BasicNumber(100.0), UnitValueFixture.kg()) + )) + + // When + val actual = sut.toDataExpression(value) + + // Then + assertEquals(value.toERecord(), actual) + } + + @Test + fun `when StringValue should map to EStringLiteral`() { + // Given + val value = StringValue("10") + + // When + val actual = sut.toDataExpression(value) + + // Then + assertEquals(value.toEStringLiteral(), actual) + } + } + + @Nested + inner class ToFromProcess { + @Test + fun `should map`() { + // given + val value = FromProcessRefValue( + "aaa", + mapOf("bbb" to StringValue("100")), + mapOf("ccc" to StringValue("200")) + ) + + // when + val actual = sut.toFromProcess(value) + + // then + assertEquals(value.name, actual.name) + + assertEquals(value.matchLabels.size, actual.matchLabels.elements.size) + assertEquals("100", actual.matchLabels.elements["bbb"].toStr()) + + assertEquals(value.arguments.size, actual.arguments.size) + assertEquals("200", actual.arguments["ccc"].toStr()) + } + } + + @Nested + inner class ToEBioExchange { + @Test + fun `when FullyQualifiedSubstanceValue should map`() { + // given + val qty = QuantityValue(BasicNumber(100.0), UnitValueFixture.kg()) + val value = FullyQualifiedSubstanceValue( + "name", + SubstanceType.EMISSION, + "comp", + "subcomp", + UnitValueFixture.kg() + ) + + // when + val actual = sut.toEBioExchange(qty, value) + + // then + assertEquals(qty.toEQuantityScale(), actual.quantity) + assertEquals(value.getShortName(), actual.substance.name) + assertEquals(value.getDisplayName(), actual.substance.displayName) + assertEquals(value.type, actual.substance.type) + assertEquals(value.compartment, actual.substance.compartment) + assertEquals(value.subcompartment, actual.substance.subCompartment) + assertEquals(value.referenceUnit.toEUnitLiteral(), actual.substance.referenceUnit) + } + + @Test + fun `when PartiallyQualifiedSubstanceValue should map`() { + // given + val qty = QuantityValue(BasicNumber(100.0), UnitValueFixture.kg()) + val value = PartiallyQualifiedSubstanceValue("name",UnitValueFixture.kg()) + + // when + val actual = sut.toEBioExchange(qty, value) + + // then + assertEquals(qty.toEQuantityScale(), actual.quantity) + assertEquals(value.getShortName(), actual.substance.name) + assertEquals(value.getDisplayName(), actual.substance.displayName) + assertEquals(value.referenceUnit.toEUnitLiteral(), actual.substance.referenceUnit) + } + } + + @Nested + inner class ToEImpact { + @Test + fun `should map`() { + // given + val qty = QuantityValue(BasicNumber(100.0), UnitValueFixture.kg()) + val value = IndicatorValue("name",UnitValueFixture.kg()) + + // when + val actual = sut.toEImpact(qty, value) + + // then + assertEquals(qty.toEQuantityScale(), actual.quantity) + assertEquals(value.name, actual.indicator.name) + assertEquals(value.referenceUnit.toEUnitLiteral(), actual.indicator.referenceUnit) + } + } + + @Nested + inner class ToETechnoExchange() { + @Test + fun `when ProductValue with QuantityValue should map`() { + // given + val qty = QuantityValue(BasicNumber(1.0), UnitValueFixture.kg()) + val processRef = FromProcessRefValue("process name") + val product = ProductValue("name", UnitValueFixture.kg(), processRef) + // when + val actual = sut.toETechnoExchange(qty, product) + + // then + assertEquals(qty.toEQuantityScale(), actual.quantity) + assertEquals(product.name, actual.product.name) + assertEquals("kg", actual.product.referenceUnit.toStr()) + assertEquals("process name", actual.product.fromProcess?.name) + } + + @Test + fun `when TechnoExchangeValue should map`() { + // given + val qty = QuantityValue(BasicNumber(3.0), UnitValueFixture.kg()) + val allocation = QuantityValue(BasicNumber(2.0), UnitValueFixture.kg()) + val processRef = FromProcessRefValue("process name") + val product = ProductValue("name", UnitValueFixture.kg(), processRef) + val value = TechnoExchangeValue(qty, product, allocation) + + // when + val actual = sut.toETechnoExchange(value) + + // then + assertEquals(qty.toEQuantityScale(), actual.quantity) + assertEquals(value.product.name, actual.product.name) + assertEquals(value.product.referenceUnit.toEUnitLiteral(), actual.product.referenceUnit) + assertEquals(processRef.name, actual.product.fromProcess?.name) + assertEquals(allocation.toEQuantityScale(), actual.allocation) + } + } +} \ No newline at end of file diff --git a/core/src/test/kotlin/ch/kleis/lcaac/core/lang/fixture/QuantityFixture.kt b/core/src/test/kotlin/ch/kleis/lcaac/core/lang/fixture/QuantityFixture.kt index 6dccb95d..806596d5 100644 --- a/core/src/test/kotlin/ch/kleis/lcaac/core/lang/fixture/QuantityFixture.kt +++ b/core/src/test/kotlin/ch/kleis/lcaac/core/lang/fixture/QuantityFixture.kt @@ -18,4 +18,5 @@ object QuantityFixture { val twoLitres = EQuantityScale(ops.pure(2.0), UnitFixture.l) val oneHour = EQuantityScale(ops.pure(1.0), UnitFixture.hour) val hundredWatt = EQuantityScale(ops.pure(100.0), UnitFixture.watt) + val fiftyPercent = EQuantityScale(ops.pure(50.0), UnitFixture.percent) } diff --git a/core/src/test/kotlin/ch/kleis/lcaac/core/lang/resolver/CachedProcessResolverTest.kt b/core/src/test/kotlin/ch/kleis/lcaac/core/lang/resolver/CachedProcessResolverTest.kt new file mode 100644 index 00000000..d5295308 --- /dev/null +++ b/core/src/test/kotlin/ch/kleis/lcaac/core/lang/resolver/CachedProcessResolverTest.kt @@ -0,0 +1,258 @@ +package ch.kleis.lcaac.core.lang.resolver + +import ch.kleis.lcaac.core.datasource.DataSourceOperations +import ch.kleis.lcaac.core.lang.SymbolTable +import ch.kleis.lcaac.core.lang.expression.* +import ch.kleis.lcaac.core.lang.fixture.IndicatorFixture.Companion.climateChange +import ch.kleis.lcaac.core.lang.fixture.ProductFixture.Companion.carrot +import ch.kleis.lcaac.core.lang.fixture.ProductFixture.Companion.salad +import ch.kleis.lcaac.core.lang.fixture.ProductFixture.Companion.water +import ch.kleis.lcaac.core.lang.fixture.QuantityFixture.fiftyPercent +import ch.kleis.lcaac.core.lang.fixture.QuantityFixture.oneKilogram +import ch.kleis.lcaac.core.lang.fixture.QuantityFixture.oneLitre +import ch.kleis.lcaac.core.lang.fixture.QuantityFixture.oneUnit +import ch.kleis.lcaac.core.lang.fixture.QuantityFixture.threeKilograms +import ch.kleis.lcaac.core.lang.fixture.QuantityFixture.twoKilograms +import ch.kleis.lcaac.core.lang.fixture.QuantityFixture.twoLitres +import ch.kleis.lcaac.core.lang.fixture.QuantityFixture.twoUnits +import ch.kleis.lcaac.core.lang.fixture.SubstanceFixture.Companion.propanol +import ch.kleis.lcaac.core.lang.fixture.UnitFixture +import ch.kleis.lcaac.core.lang.register.ProcessKey +import ch.kleis.lcaac.core.lang.register.ProcessTemplateRegister +import ch.kleis.lcaac.core.math.basic.BasicNumber +import ch.kleis.lcaac.core.math.basic.BasicOperations +import io.mockk.mockk +import kotlin.test.Test +import kotlin.test.assertEquals + +class CachedProcessResolverTest { + private val ops = BasicOperations + private val sourceOps = mockk>() + + @Test + fun `when process without dependencies should return same process`() { + // Given + val template = EProcessTemplate( + body = EProcess( + name = "carrot_production", + products = listOf( + ETechnoExchange(twoKilograms, carrot, fiftyPercent), + ETechnoExchange(threeKilograms, salad, fiftyPercent), + ), + inputs = listOf( + ETechnoBlockEntry(ETechnoExchange(twoLitres, water)), + ), + biosphere = listOf( + EBioBlockEntry(EBioExchange(oneKilogram, propanol)) + ), + impacts = listOf( + EImpactBlockEntry(EImpact(oneUnit, climateChange)), + ) + ) + ) + val spec = carrot + val symbolTable = SymbolTable( + processTemplates = ProcessTemplateRegister.from( + mapOf( + ProcessKey("carrot_production") to template, + ) + ) + ) + val sut = CachedProcessResolver(symbolTable, ops, sourceOps) + + // When + val actual = sut.resolve(template, spec) + + // Then + assertEquals(2, actual.products.size) + assertEquals(twoKilograms, actual.products[0].quantity) + assertEquals(carrot.name, actual.products[0].product.name) + assertEquals(threeKilograms, actual.products[1].quantity) + assertEquals(salad.name, actual.products[1].product.name) + + assertEquals(1, actual.inputs.size) + assertEquals(twoLitres, (actual.inputs[0] as ETechnoBlockEntry).entry.quantity) + assertEquals(water.name, (actual.inputs[0] as ETechnoBlockEntry).entry.product.name) + + assertEquals(1, actual.biosphere.size) + assertEquals(oneKilogram, (actual.biosphere[0] as EBioBlockEntry).entry.quantity) + assertEquals(propanol.name, (actual.biosphere[0] as EBioBlockEntry).entry.substance.name) + + assertEquals(1, actual.impacts.size) + assertEquals(oneUnit, (actual.impacts[0] as EImpactBlockEntry).entry.quantity) + assertEquals(climateChange.name, (actual.impacts[0] as EImpactBlockEntry).entry.indicator.name) + } + + @Test + fun `when process with dependencies should return process without dependencies`() { + // Given + val template = EProcessTemplate( + body = EProcess( + name = "carrot_production", + products = listOf( + ETechnoExchange(twoKilograms, carrot, fiftyPercent), + ETechnoExchange(threeKilograms, salad, fiftyPercent), + ), + inputs = listOf( + ETechnoBlockEntry(ETechnoExchange(twoLitres, water)), + ), + biosphere = listOf( + EBioBlockEntry(EBioExchange(oneKilogram, propanol)) + ), + impacts = listOf( + EImpactBlockEntry(EImpact(oneUnit, climateChange)), + ) + ) + ) + + val waterTemplate = EProcessTemplate( + body = EProcess( + name = "water_production", + products = listOf( + ETechnoExchange(oneLitre, water) + ), + inputs = listOf( + ETechnoBlockEntry( + ETechnoExchange( + twoKilograms, EProductSpec( + "detergent", oneKilogram + ) + ) + ), + ), + biosphere = listOf( + EBioBlockEntry(EBioExchange(twoKilograms, propanol)) + ), + impacts = listOf( + EImpactBlockEntry(EImpact(twoUnits, climateChange)), + ) + ) + ) + + val spec = carrot + val symbolTable = SymbolTable( + processTemplates = ProcessTemplateRegister.from( + mapOf( + ProcessKey("carrot_production") to template, + ProcessKey("water_production") to waterTemplate + ) + ) + ) + val sut = CachedProcessResolver(symbolTable, ops, sourceOps) + + // When + val actual = sut.resolve(template, spec) + + // Then + assertEquals(2, actual.products.size) + assertEquals(twoKilograms, actual.products[0].quantity) + assertEquals(carrot.name, actual.products[0].product.name) + assertEquals(threeKilograms, actual.products[1].quantity) + assertEquals(salad.name, actual.products[1].product.name) + + assertEquals(1, actual.inputs.size) + assertEquals( + EQuantityScale( + ops.pure(4.0), + UnitFixture.kg + ), (actual.inputs[0] as ETechnoBlockEntry).entry.quantity + ) + assertEquals("detergent", (actual.inputs[0] as ETechnoBlockEntry).entry.product.name) + + assertEquals(1, actual.biosphere.size) + // 2 * 2 propanol from water biosphere + 1 propanol from carrot biosphere + assertEquals( + EQuantityScale( + ops.pure(5.0), + UnitFixture.kg + ), (actual.biosphere[0] as EBioBlockEntry).entry.quantity + ) + assertEquals(propanol.name, (actual.biosphere[0] as EBioBlockEntry).entry.substance.name) + + assertEquals(1, actual.impacts.size) + assertEquals( + // 2 * 2 climate change from water impact + 1 climate change from carrot impact + EQuantityScale( + ops.pure(5.0), + UnitFixture.unit + ), (actual.impacts[0] as EImpactBlockEntry).entry.quantity + ) + assertEquals(climateChange.name, (actual.impacts[0] as EImpactBlockEntry).entry.indicator.name) + } + + @Test + fun `when process with deep dependencies should return process without dependencies`() { + // Given + val template = EProcessTemplate( + body = EProcess( + name = "carrot_production", + products = listOf( + ETechnoExchange(twoKilograms, carrot, fiftyPercent), + ETechnoExchange(threeKilograms, salad, fiftyPercent), + ), + inputs = listOf( + ETechnoBlockEntry(ETechnoExchange(twoLitres, water)), + ) + ) + ) + val detergent = EProductSpec("detergent", oneKilogram) + val waterTemplate = EProcessTemplate( + body = EProcess( + name = "water_production", + products = listOf( + ETechnoExchange(oneLitre, water) + ), + inputs = listOf( + ETechnoBlockEntry(ETechnoExchange(twoKilograms, detergent)), + ) + ) + ) + + val detergentTemplate = EProcessTemplate( + body = EProcess( + name = "detergent_production", + products = listOf( + ETechnoExchange(oneKilogram, detergent), + ), + impacts = listOf( + EImpactBlockEntry(EImpact(twoUnits, climateChange)), + ) + ) + ) + + val spec = carrot + val symbolTable = SymbolTable( + processTemplates = ProcessTemplateRegister.from( + mapOf( + ProcessKey("carrot_production") to template, + ProcessKey("water_production") to waterTemplate, + ProcessKey("detergent_production") to detergentTemplate + ) + ) + ) + val sut = CachedProcessResolver(symbolTable, ops, sourceOps) + + // When + val actual = sut.resolve(template, spec) + + // Then + assertEquals(2, actual.products.size) + assertEquals(twoKilograms, actual.products[0].quantity) + assertEquals(carrot.name, actual.products[0].product.name) + assertEquals(threeKilograms, actual.products[1].quantity) + assertEquals(salad.name, actual.products[1].product.name) + + assertEquals(0, actual.inputs.size) + assertEquals(0, actual.biosphere.size) + + assertEquals(1, actual.impacts.size) + assertEquals( + // 2 * 2 * 2 climate change from detergent impact + EQuantityScale( + ops.pure(8.0), + UnitFixture.unit + ), (actual.impacts[0] as EImpactBlockEntry).entry.quantity + ) + assertEquals(climateChange.name, (actual.impacts[0] as EImpactBlockEntry).entry.indicator.name) + } +} \ No newline at end of file diff --git a/core/src/test/kotlin/ch/kleis/lcaac/core/lang/resolver/ProcessResolverTest.kt b/core/src/test/kotlin/ch/kleis/lcaac/core/lang/resolver/ProcessTemplateResolverTest.kt similarity index 96% rename from core/src/test/kotlin/ch/kleis/lcaac/core/lang/resolver/ProcessResolverTest.kt rename to core/src/test/kotlin/ch/kleis/lcaac/core/lang/resolver/ProcessTemplateResolverTest.kt index 186d899a..0726b453 100644 --- a/core/src/test/kotlin/ch/kleis/lcaac/core/lang/resolver/ProcessResolverTest.kt +++ b/core/src/test/kotlin/ch/kleis/lcaac/core/lang/resolver/ProcessTemplateResolverTest.kt @@ -11,7 +11,7 @@ import org.junit.jupiter.api.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith -class ProcessResolverTest { +class ProcessTemplateResolverTest { @Test fun resolve_withLabelMatching() { // given @@ -50,7 +50,7 @@ class ProcessResolverTest { val symbolTable = SymbolTable( processTemplates = processTemplates, ) - val resolver = ProcessResolver(symbolTable) + val resolver = ProcessTemplateResolver(symbolTable) // when val actual = resolver.resolve(carrotSpec) @@ -91,7 +91,7 @@ class ProcessResolverTest { val symbolTable = SymbolTable( processTemplates = processTemplates, ) - val resolver = ProcessResolver(symbolTable) + val resolver = ProcessTemplateResolver(symbolTable) // when/then val e = assertFailsWith(EvaluatorException::class) { @@ -143,7 +143,7 @@ class ProcessResolverTest { val symbolTable = SymbolTable( processTemplates = processTemplates, ) - val resolver = ProcessResolver(symbolTable) + val resolver = ProcessTemplateResolver(symbolTable) // when val actual = resolver.resolve(carrotSpec) @@ -193,7 +193,7 @@ class ProcessResolverTest { val symbolTable = SymbolTable( processTemplates = processTemplates, ) - val resolver = ProcessResolver(symbolTable) + val resolver = ProcessTemplateResolver(symbolTable) // when val actual = resolver.resolve(carrotSpec) @@ -252,7 +252,7 @@ class ProcessResolverTest { val symbolTable = SymbolTable( processTemplates = processTemplates, ) - val resolver = ProcessResolver(symbolTable) + val resolver = ProcessTemplateResolver(symbolTable) // {w,t}hen val exception = assertFailsWith(EvaluatorException::class) { resolver.resolve(carrotSpec) } diff --git a/core/src/test/kotlin/ch/kleis/lcaac/core/matrix/ImpactFactorMatrixTest.kt b/core/src/test/kotlin/ch/kleis/lcaac/core/matrix/ImpactFactorMatrixTest.kt index aafc379c..31524101 100644 --- a/core/src/test/kotlin/ch/kleis/lcaac/core/matrix/ImpactFactorMatrixTest.kt +++ b/core/src/test/kotlin/ch/kleis/lcaac/core/matrix/ImpactFactorMatrixTest.kt @@ -1,5 +1,6 @@ package ch.kleis.lcaac.core.matrix +import ch.kleis.lcaac.core.lang.expression.SubstanceType import ch.kleis.lcaac.core.lang.fixture.UnitFixture import ch.kleis.lcaac.core.lang.value.* import ch.kleis.lcaac.core.math.basic.BasicNumber @@ -76,4 +77,68 @@ class ImpactFactorMatrixTest { // Then assertEquals(6, result) } + + @Test + fun getIndicators() { + // Given + val input1 = ProductValue("oil", literValue, null) + val input2 = IndicatorValue("water", literValue) + val input3 = PartiallyQualifiedSubstanceValue("pqsv", literValue) + val input4 = FullyQualifiedSubstanceValue("fqsv", SubstanceType.EMISSION, "", "", literValue) + val inputs: IndexedCollection> = + IndexedCollection(listOf(input1, input2, input3, input4)) + val data = MatrixFixture.basic(0, 0, arrayOf() + ) + val sut = ImpactFactorMatrix(outputs, inputs, data, ops) + + // When + val actual = sut.getIndicators() + + // Then + assertEquals(1, actual.size) + assertEquals(input2, actual[0]) + } + + @Test + fun getSubstances() { + // Given + val input1 = ProductValue("oil", literValue, null) + val input2 = IndicatorValue("water", literValue) + val input3 = PartiallyQualifiedSubstanceValue("pqsv", literValue) + val input4 = FullyQualifiedSubstanceValue("fqsv", SubstanceType.EMISSION, "", "", literValue) + val inputs: IndexedCollection> = + IndexedCollection(listOf(input1, input2, input3, input4)) + val data = MatrixFixture.basic(0, 0, arrayOf() + ) + val sut = ImpactFactorMatrix(outputs, inputs, data, ops) + + // When + val actual = sut.getSubstances() + + // Then + assertEquals(2, actual.size) + assertEquals(input3, actual[0]) + assertEquals(input4, actual[1]) + } + + @Test + fun getInputProducts() { + // Given + val input1 = ProductValue("oil", literValue, null) + val input2 = IndicatorValue("water", literValue) + val input3 = PartiallyQualifiedSubstanceValue("pqsv", literValue) + val input4 = FullyQualifiedSubstanceValue("fqsv", SubstanceType.EMISSION, "", "", literValue) + val inputs: IndexedCollection> = + IndexedCollection(listOf(input1, input2, input3, input4)) + val data = MatrixFixture.basic(0, 0, arrayOf() + ) + val sut = ImpactFactorMatrix(outputs, inputs, data, ops) + + // When + val actual = sut.getInputProducts() + + // Then + assertEquals(1, actual.size) + assertEquals(input1, actual[0]) + } } diff --git a/core/src/test/kotlin/ch/kleis/lcaac/core/testing/BasicTestRunnerTest.kt b/core/src/test/kotlin/ch/kleis/lcaac/core/testing/BasicTestRunnerTest.kt index b2752363..5bfd9b04 100644 --- a/core/src/test/kotlin/ch/kleis/lcaac/core/testing/BasicTestRunnerTest.kt +++ b/core/src/test/kotlin/ch/kleis/lcaac/core/testing/BasicTestRunnerTest.kt @@ -9,6 +9,7 @@ import ch.kleis.lcaac.core.lang.fixture.ProcessFixture import ch.kleis.lcaac.core.lang.fixture.UnitFixture import ch.kleis.lcaac.core.lang.fixture.UnitValueFixture import ch.kleis.lcaac.core.lang.value.QuantityValue +import ch.kleis.lcaac.core.math.basic.BasicMatrix import ch.kleis.lcaac.core.math.basic.BasicNumber import io.mockk.every import io.mockk.mockk @@ -138,7 +139,7 @@ class BasicTestRunnerTest { val carrotProduction = EProcessTemplate( body = ProcessFixture.carrotProduction ) - val evaluator = mockk>() + val evaluator = mockk>() every { evaluator.with(carrotProduction) } returns evaluator every { evaluator.trace(carrotProduction) } throws EvaluatorException("some error") val runner = BasicTestRunner(SymbolTable.empty(), mockk(), evaluator) diff --git a/grammar/src/main/antlr/LcaLang.g4 b/grammar/src/main/antlr/LcaLang.g4 index 9cebfa71..88518196 100644 --- a/grammar/src/main/antlr/LcaLang.g4 +++ b/grammar/src/main/antlr/LcaLang.g4 @@ -136,12 +136,20 @@ meta_assignment : STRING_LITERAL EQUAL STRING_LITERAL ; +/* + Annotation +*/ + +annotation + : AT_CACHE_KEYWORD + ; + /* Process */ processDefinition - : PROCESS_KEYWORD name=processRef LBRACE + : annotation* PROCESS_KEYWORD name=processRef LBRACE ( params | labels @@ -361,6 +369,7 @@ RESOURCES_KEYWORD : 'resources' ; MATCH_KEYWORD : 'match' ; WHERE_KEYWORD : 'where' ; LABELS_KEYWORD : 'labels' ; +AT_CACHE_KEYWORD: '@cached' ; DATASOURCE_KEYWORD : 'datasource' ; LOCATION : 'location' ; diff --git a/grammar/src/main/kotlin/ch/kleis/lcaac/grammar/CoreMapper.kt b/grammar/src/main/kotlin/ch/kleis/lcaac/grammar/CoreMapper.kt index bb00ddcd..55e5a500 100644 --- a/grammar/src/main/kotlin/ch/kleis/lcaac/grammar/CoreMapper.kt +++ b/grammar/src/main/kotlin/ch/kleis/lcaac/grammar/CoreMapper.kt @@ -24,6 +24,8 @@ class CoreMapper( ): EProcessTemplate { val name = ctx.name.innerText() val labels = ctx.labels().flatMap { it.label_assignment() }.associate { it.labelRef().innerText() to EStringLiteral(it.STRING_LITERAL().innerText()) } + val annotations = ctx.annotation().mapNotNull { toProcessAnnotation(it.text) }.toSet() + val locals = ctx.variables().flatMap { it.assignment() }.associate { assignment(it) } val params = ctx.params().flatMap { it.assignment() }.associate { assignment(it) } val symbolTable = SymbolTable( @@ -53,6 +55,7 @@ class CoreMapper( params, locals, body, + annotations ) } @@ -392,4 +395,11 @@ class CoreMapper( schema = schema, ) } + + fun toProcessAnnotation(value: String): ProcessAnnotation { + return when (value) { + "@cached" -> ProcessAnnotation.CACHED + else -> throw EvaluatorException("Invalid process annotation: $value") + } + } } diff --git a/grammar/src/test/kotlin/ch/kleis/lcaac/grammar/CoreMapperTest.kt b/grammar/src/test/kotlin/ch/kleis/lcaac/grammar/CoreMapperTest.kt index ac7cf3e1..c1abd671 100644 --- a/grammar/src/test/kotlin/ch/kleis/lcaac/grammar/CoreMapperTest.kt +++ b/grammar/src/test/kotlin/ch/kleis/lcaac/grammar/CoreMapperTest.kt @@ -2,12 +2,18 @@ package ch.kleis.lcaac.grammar import ch.kleis.lcaac.core.config.DataSourceConfig import ch.kleis.lcaac.core.lang.SymbolTable +import ch.kleis.lcaac.core.lang.evaluator.EvaluatorException import ch.kleis.lcaac.core.lang.expression.* +import ch.kleis.lcaac.core.lang.expression.ProcessAnnotation.CACHED +import ch.kleis.lcaac.core.lang.register.DataRegister import ch.kleis.lcaac.core.math.basic.BasicNumber import ch.kleis.lcaac.core.math.basic.BasicOperations +import ch.kleis.lcaac.grammar.parser.LcaLangParser import io.mockk.mockk +import org.junit.jupiter.api.Nested import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.fail class CoreMapperTest { private val ops = BasicOperations @@ -301,4 +307,59 @@ class CoreMapperTest { )))) assertEquals(expected, actual) } + + @Test + fun load_process_with_cached_annotation() { + // given + val ctx = LcaLangFixture.parser( + """ + @cached + process p { + } + """.trimIndent()).processDefinition() + val mapper = CoreMapper(ops) + + // when + val actual = mapper.process(ctx, DataRegister.empty(), DataRegister.empty()) + + // then + val expected = EProcessTemplate( + body = EProcess( + "p", + ), + annotations = setOf(CACHED) + ) + assertEquals(expected, actual) + } + + @Nested + inner class ToProcessAnnotation { + @Test + fun `when cached return cached annotation`() { + // given + val annotation = "@cached" + val mapper = CoreMapper(ops) + + // when + val actual = mapper.toProcessAnnotation(annotation) + + // then + assertEquals(CACHED, actual) + } + + @Test + fun `when invalid value throw`() { + // given + val annotation = "@invalid_annotation" + val mapper = CoreMapper(ops) + + // when + then + try { + mapper.toProcessAnnotation(annotation) + fail("Expected an EvaluatorException to be thrown") + } catch (e: EvaluatorException) { + assertEquals("Invalid process annotation: @invalid_annotation", e.message) + } + } + } } diff --git a/grammar/src/test/kotlin/ch/kleis/lcaac/grammar/EvaluatorTest.kt b/grammar/src/test/kotlin/ch/kleis/lcaac/grammar/EvaluatorTest.kt index 23066f3d..9b53016e 100644 --- a/grammar/src/test/kotlin/ch/kleis/lcaac/grammar/EvaluatorTest.kt +++ b/grammar/src/test/kotlin/ch/kleis/lcaac/grammar/EvaluatorTest.kt @@ -405,4 +405,71 @@ class EvaluatorTest { assertEquals(expected, actual) } + + @Test + fun cachedProcess() { + // given + val content = """ + process main { + products { + 1 kg carrot + } + inputs { + 2 kg grass + 3 l fertilizer_a + } + emissions { + 1 kg co2 + } + } + + process bar { + products { + 1 kg grass + } + inputs { + 4 kg fertilizer_b + } + emissions { + 5 l bar + } + impacts { + 6 kg foo + } + } + """.trimIndent() + + val fileWithoutCache = LcaLangFixture.parser(content).lcaFile() + val fileWithCache = LcaLangFixture.parser("@cached\n$content").lcaFile() + + val loader = Loader(ops) + val spec = EProductSpec(name = "carrot") + + val expectedSymbolTable = loader.load(sequenceOf(fileWithoutCache), listOf(LoaderOption.WITH_PRELUDE)) + val expectedTrace = Evaluator(expectedSymbolTable, ops, sourceOps).trace(setOf(spec)) + val expectedProgram = ContributionAnalysisProgram(expectedTrace.getSystemValue(), expectedTrace.getEntryPoint()) + val expectedAnalysis = expectedProgram.run() + + // when + val actualSymbolTable = loader.load(sequenceOf(fileWithCache), listOf(LoaderOption.WITH_PRELUDE)) + val actualTrace = Evaluator(actualSymbolTable, ops, sourceOps).trace(setOf(spec)) + val actualProgram = ContributionAnalysisProgram(actualTrace.getSystemValue(), actualTrace.getEntryPoint()) + val actualAnalysis = actualProgram.run() + + // then + assertEquals(1, actualAnalysis.getObservablePorts().size()) + assertEquals(expectedAnalysis.getObservablePorts().get("carrot from main{}{}"),actualAnalysis.getObservablePorts().get("carrot from main{}{}")) + + val port = expectedAnalysis.getObservablePorts().get("carrot from main{}{}") + val indicators = listOf( + expectedAnalysis.getControllablePorts().get("fertilizer_a"), + expectedAnalysis.getControllablePorts().get("fertilizer_b"), + expectedAnalysis.getControllablePorts().get("bar"), + expectedAnalysis.getControllablePorts().get("co2"), + expectedAnalysis.getControllablePorts().get("foo"), + ) + indicators.forEach { indicator -> + assertEquals(expectedAnalysis.getPortContribution(port, indicator), actualAnalysis.getPortContribution(port, indicator)) + } + } } diff --git a/grammar/src/test/kotlin/ch/kleis/lcaac/grammar/LoaderTest.kt b/grammar/src/test/kotlin/ch/kleis/lcaac/grammar/LoaderTest.kt index 756cb83d..91004360 100644 --- a/grammar/src/test/kotlin/ch/kleis/lcaac/grammar/LoaderTest.kt +++ b/grammar/src/test/kotlin/ch/kleis/lcaac/grammar/LoaderTest.kt @@ -4,7 +4,9 @@ import ch.kleis.lcaac.core.config.DataSourceConfig import ch.kleis.lcaac.core.lang.SymbolTable import ch.kleis.lcaac.core.lang.dimension.Dimension import ch.kleis.lcaac.core.lang.dimension.UnitSymbol +import ch.kleis.lcaac.core.lang.evaluator.EvaluatorException import ch.kleis.lcaac.core.lang.expression.* +import ch.kleis.lcaac.core.lang.expression.ProcessAnnotation.CACHED import ch.kleis.lcaac.core.lang.register.DataKey import ch.kleis.lcaac.core.lang.register.DataRegister import ch.kleis.lcaac.core.math.basic.BasicNumber @@ -12,6 +14,7 @@ import ch.kleis.lcaac.core.math.basic.BasicOperations import org.junit.jupiter.api.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull +import kotlin.test.fail class LoaderTest { @Test