diff --git a/cli/src/main/kotlin/ch/kleis/lcaac/cli/cmd/AssessCommand.kt b/cli/src/main/kotlin/ch/kleis/lcaac/cli/cmd/AssessCommand.kt index 1e598a8c..920036e4 100644 --- a/cli/src/main/kotlin/ch/kleis/lcaac/cli/cmd/AssessCommand.kt +++ b/cli/src/main/kotlin/ch/kleis/lcaac/cli/cmd/AssessCommand.kt @@ -44,7 +44,7 @@ class AssessCommand : CliktCommand(name = "assess", help = "Returns the unitary override fun run() { val files = lcaFiles(path) val symbolTable = Loader(BasicOperations).load(files, listOf(LoaderOption.WITH_PRELUDE)) - val processor = CsvProcessor(symbolTable) + val processor = CsvProcessor(path, symbolTable) val iterator = loadRequests() val writer = CsvResultWriter() var first = true diff --git a/cli/src/main/kotlin/ch/kleis/lcaac/cli/cmd/TestCommand.kt b/cli/src/main/kotlin/ch/kleis/lcaac/cli/cmd/TestCommand.kt index 7d9cfb6c..59d483c6 100644 --- a/cli/src/main/kotlin/ch/kleis/lcaac/cli/cmd/TestCommand.kt +++ b/cli/src/main/kotlin/ch/kleis/lcaac/cli/cmd/TestCommand.kt @@ -1,5 +1,6 @@ package ch.kleis.lcaac.cli.cmd +import ch.kleis.lcaac.core.datasource.CsvSourceOperations import ch.kleis.lcaac.core.math.basic.BasicOperations import ch.kleis.lcaac.core.testing.BasicTestRunner import ch.kleis.lcaac.core.testing.GenericFailure @@ -32,12 +33,15 @@ class TestCommand : CliktCommand(name = "test", help = "Run specified tests") { override fun run() { val files = lcaFiles(path) - val symbolTable = Loader(BasicOperations).load(files, listOf(LoaderOption.WITH_PRELUDE)) + val ops = BasicOperations + val symbolTable = Loader(ops).load(files, listOf(LoaderOption.WITH_PRELUDE)) val mapper = CoreTestMapper() val cases = files .flatMap { it.testDefinition() } .map { mapper.test(it) } - val runner = BasicTestRunner(symbolTable) + val runner = BasicTestRunner( + symbolTable, + CsvSourceOperations(path, ops)) val results = cases.map { runner.run(it) } results.forEach { result -> diff --git a/cli/src/main/kotlin/ch/kleis/lcaac/cli/cmd/Utils.kt b/cli/src/main/kotlin/ch/kleis/lcaac/cli/cmd/Utils.kt index 0ab757a4..143060fb 100644 --- a/cli/src/main/kotlin/ch/kleis/lcaac/cli/cmd/Utils.kt +++ b/cli/src/main/kotlin/ch/kleis/lcaac/cli/cmd/Utils.kt @@ -3,8 +3,6 @@ package ch.kleis.lcaac.cli.cmd import ch.kleis.lcaac.core.lang.evaluator.EvaluatorException import ch.kleis.lcaac.core.lang.expression.DataExpression import ch.kleis.lcaac.core.lang.expression.EQuantityScale -import ch.kleis.lcaac.core.lang.expression.EUnitOf -import ch.kleis.lcaac.core.lang.expression.QuantityExpression import ch.kleis.lcaac.core.math.basic.BasicNumber import ch.kleis.lcaac.core.math.basic.BasicOperations import ch.kleis.lcaac.grammar.CoreMapper @@ -34,31 +32,26 @@ private fun lcaFile(inputStream: InputStream): LcaLangParser.LcaFileContext { return parser.lcaFile() } -fun parseQuantityWithDefaultUnit(s: String, defaultUnit: DataExpression): DataExpression { +fun smartParseQuantityWithDefaultUnit(s: String, defaultUnit: DataExpression): DataExpression { val parts = s.split(" ") - return when (parts.size) { - 1 -> { - val number = parts[0] - val amount = try { - parseDouble(number) - } catch (e: NumberFormatException) { - throw EvaluatorException("'$s' is not a valid quantity") - } - EQuantityScale(BasicNumber(amount), defaultUnit) + return if (parts.size == 1) { + val number = parts[0] + val amount = try { + parseDouble(number) + } catch (e: NumberFormatException) { + throw EvaluatorException("'$s' is not a valid quantity") } - 2 -> { - val lexer = LcaLangLexer(CharStreams.fromString(s)) - val tokens = CommonTokenStream(lexer) - val parser = LcaLangParser(tokens) - val ctx = parser.dataExpression() - try { - CoreMapper(BasicOperations).dataExpression(ctx) - } catch (e: IllegalStateException) { - throw EvaluatorException("'$s' is not a valid quantity") - } + EQuantityScale(BasicNumber(amount), defaultUnit) + } else { + val lexer = LcaLangLexer(CharStreams.fromString(s)) + val tokens = CommonTokenStream(lexer) + val parser = LcaLangParser(tokens) + val ctx = parser.dataExpression() + try { + CoreMapper(BasicOperations).dataExpression(ctx) + } catch (e: IllegalStateException) { + throw EvaluatorException("'$s' is not a valid quantity") } - else -> throw EvaluatorException("'$s' is not a valid quantity") } - } diff --git a/cli/src/main/kotlin/ch/kleis/lcaac/cli/csv/CsvProcessor.kt b/cli/src/main/kotlin/ch/kleis/lcaac/cli/csv/CsvProcessor.kt index 7cf631b9..f064abb8 100644 --- a/cli/src/main/kotlin/ch/kleis/lcaac/cli/csv/CsvProcessor.kt +++ b/cli/src/main/kotlin/ch/kleis/lcaac/cli/csv/CsvProcessor.kt @@ -1,7 +1,8 @@ package ch.kleis.lcaac.cli.csv -import ch.kleis.lcaac.cli.cmd.parseQuantityWithDefaultUnit +import ch.kleis.lcaac.cli.cmd.smartParseQuantityWithDefaultUnit import ch.kleis.lcaac.core.assessment.ContributionAnalysisProgram +import ch.kleis.lcaac.core.datasource.CsvSourceOperations import ch.kleis.lcaac.core.lang.SymbolTable import ch.kleis.lcaac.core.lang.evaluator.Evaluator import ch.kleis.lcaac.core.lang.evaluator.EvaluatorException @@ -11,34 +12,37 @@ import ch.kleis.lcaac.core.lang.expression.QuantityExpression import ch.kleis.lcaac.core.lang.expression.StringExpression import ch.kleis.lcaac.core.math.basic.BasicNumber import ch.kleis.lcaac.core.math.basic.BasicOperations +import java.io.File class CsvProcessor( - private val symbolTable: SymbolTable, + path: File, + private val symbolTable: SymbolTable, ) { private val ops = BasicOperations - private val evaluator = Evaluator(symbolTable, ops) + private val sourceOps = CsvSourceOperations(path, ops) + private val evaluator = Evaluator(symbolTable, ops, sourceOps) fun process(request: CsvRequest): List { val reqName = request.processName val reqLabels = request.matchLabels val template = - symbolTable.getTemplate(reqName, reqLabels) - ?: throw EvaluatorException("Could not get template for ${reqName}${reqLabels}") + symbolTable.getTemplate(reqName, reqLabels) + ?: throw EvaluatorException("Could not get template for ${reqName}${reqLabels}") val arguments = template.params - .mapValues { entry -> - when (val v = entry.value) { - is QuantityExpression<*> -> request[entry.key]?.let { - parseQuantityWithDefaultUnit(it, EUnitOf(v)) - } ?: entry.value + .mapValues { entry -> + when (val v = entry.value) { + is QuantityExpression<*> -> request[entry.key]?.let { + smartParseQuantityWithDefaultUnit(it, EUnitOf(v)) + } ?: entry.value - is StringExpression -> request[entry.key]?.let { - EStringLiteral(it) - } ?: entry.value + is StringExpression -> request[entry.key]?.let { + EStringLiteral(it) + } ?: entry.value - else -> throw EvaluatorException("$v is not a supported data expression") - } + else -> throw EvaluatorException("$v is not a supported data expression") } + } val trace = evaluator.trace(template, arguments) val systemValue = trace.getSystemValue() @@ -46,14 +50,14 @@ class CsvProcessor( val program = ContributionAnalysisProgram(systemValue, entryPoint) val analysis = program.run() return entryPoint.products - .map { output -> - val outputPort = output.product - val impacts = analysis.getUnitaryImpacts(outputPort) - CsvResult( - request, - outputPort, - impacts, - ) - } + .map { output -> + val outputPort = output.product + val impacts = analysis.getUnitaryImpacts(outputPort) + CsvResult( + request, + outputPort, + impacts, + ) + } } } diff --git a/cli/src/main/kotlin/ch/kleis/lcaac/cli/csv/Utils.kt b/cli/src/main/kotlin/ch/kleis/lcaac/cli/csv/Utils.kt new file mode 100644 index 00000000..43a76c28 --- /dev/null +++ b/cli/src/main/kotlin/ch/kleis/lcaac/cli/csv/Utils.kt @@ -0,0 +1,10 @@ +package ch.kleis.lcaac.cli.csv + +import ch.kleis.lcaac.core.lang.evaluator.EvaluatorException +import ch.kleis.lcaac.core.lang.expression.DataExpression +import ch.kleis.lcaac.core.lang.expression.EQuantityScale +import ch.kleis.lcaac.core.math.basic.BasicNumber +import java.lang.Double +import kotlin.NumberFormatException +import kotlin.String + diff --git a/cli/src/test/kotlin/ch/kleis/lcaac/cli/cmd/UtilsKtTest.kt b/cli/src/test/kotlin/ch/kleis/lcaac/cli/cmd/UtilsKtTest.kt index 857f3fb2..b8b89af0 100644 --- a/cli/src/test/kotlin/ch/kleis/lcaac/cli/cmd/UtilsKtTest.kt +++ b/cli/src/test/kotlin/ch/kleis/lcaac/cli/cmd/UtilsKtTest.kt @@ -2,6 +2,7 @@ package ch.kleis.lcaac.cli.cmd import ch.kleis.lcaac.core.lang.evaluator.EvaluatorException import ch.kleis.lcaac.core.lang.expression.EDataRef +import ch.kleis.lcaac.core.lang.expression.EQuantityMul import ch.kleis.lcaac.core.lang.expression.EQuantityScale import ch.kleis.lcaac.core.math.basic.BasicNumber import ch.kleis.lcaac.core.prelude.Prelude @@ -19,7 +20,7 @@ class UtilsKtTest { val defaultUnit = Prelude.unitMap()["kg"]!! // when/then - val actual = assertThrows { parseQuantityWithDefaultUnit(s, defaultUnit) } + val actual = assertThrows { smartParseQuantityWithDefaultUnit(s, defaultUnit) } assertEquals("'a@bc' is not a valid quantity", actual.message) } @@ -30,7 +31,7 @@ class UtilsKtTest { val defaultUnit = Prelude.unitMap()["kg"]!! // when/then - val actual = assertThrows { parseQuantityWithDefaultUnit(s, defaultUnit) } + val actual = assertThrows { smartParseQuantityWithDefaultUnit(s, defaultUnit) } assertEquals("'12 3 4' is not a valid quantity", actual.message) } @@ -41,7 +42,7 @@ class UtilsKtTest { val defaultUnit = Prelude.unitMap()["kg"]!! // when/then - val actual = assertThrows { parseQuantityWithDefaultUnit(s, defaultUnit) } + val actual = assertThrows { smartParseQuantityWithDefaultUnit(s, defaultUnit) } assertEquals("'12 \$3' is not a valid quantity", actual.message) } @@ -52,7 +53,7 @@ class UtilsKtTest { val defaultUnit = Prelude.unitMap()["kg"]!! // when - val actual = parseQuantityWithDefaultUnit(s, defaultUnit) + val actual = smartParseQuantityWithDefaultUnit(s, defaultUnit) // then assertEquals(EQuantityScale(BasicNumber(12.0), defaultUnit), actual) @@ -65,9 +66,28 @@ class UtilsKtTest { val defaultUnit = Prelude.unitMap()["kg"]!! // when - val actual = parseQuantityWithDefaultUnit(s, defaultUnit) + val actual = smartParseQuantityWithDefaultUnit(s, defaultUnit) // then assertEquals(EQuantityScale(BasicNumber(12.0), EDataRef("kg")), actual) } + + @Test + fun parseQuantityWithDefaultUnit_whenComplexExpression() { + // given + val s = "12.0 kg * hour" + val kg = Prelude.unitMap()["kg"]!! + val hour = Prelude.unitMap()["hour"]!! + val defaultUnit = EQuantityMul(kg, hour) + + // when + val actual = smartParseQuantityWithDefaultUnit(s, defaultUnit) + + // then + assertEquals( + EQuantityScale( + BasicNumber(12.0), + EQuantityMul(EDataRef("kg"), EDataRef("hour"))), + actual) + } } diff --git a/cli/src/test/kotlin/ch/kleis/lcaac/cli/csv/CsvProcessorTest.kt b/cli/src/test/kotlin/ch/kleis/lcaac/cli/csv/CsvProcessorTest.kt index 4bdbca0a..3753d840 100644 --- a/cli/src/test/kotlin/ch/kleis/lcaac/cli/csv/CsvProcessorTest.kt +++ b/cli/src/test/kotlin/ch/kleis/lcaac/cli/csv/CsvProcessorTest.kt @@ -6,15 +6,16 @@ import ch.kleis.lcaac.core.lang.dimension.UnitSymbol import ch.kleis.lcaac.core.lang.value.* import ch.kleis.lcaac.core.math.basic.BasicNumber import ch.kleis.lcaac.core.math.basic.BasicOperations -import ch.kleis.lcaac.core.prelude.Prelude import ch.kleis.lcaac.grammar.Loader import ch.kleis.lcaac.grammar.LoaderOption import ch.kleis.lcaac.grammar.parser.LcaLangLexer import ch.kleis.lcaac.grammar.parser.LcaLangParser +import io.mockk.mockk import org.antlr.v4.runtime.CharStreams import org.antlr.v4.runtime.CommonTokenStream import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test +import java.io.File class CsvProcessorTest { @@ -34,7 +35,8 @@ class CsvProcessorTest { } """.trimIndent() val symbolTable = load(content) - val processor = CsvProcessor(symbolTable) + val path = mockk() + val processor = CsvProcessor(path, symbolTable) val request = CsvRequest( "main", emptyMap(), diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 49b57e17..33fbda11 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -48,6 +48,8 @@ dependencies { implementation("org.apache.logging.log4j:log4j-core:$log4jVersion") implementation("org.apache.logging.log4j:log4j-slf4j-impl:$log4jVersion") + implementation("org.apache.commons:commons-csv:1.10.0") + testImplementation(kotlin("test")) } diff --git a/core/src/main/kotlin/ch/kleis/lcaac/core/datasource/CsvSourceOperations.kt b/core/src/main/kotlin/ch/kleis/lcaac/core/datasource/CsvSourceOperations.kt new file mode 100644 index 00000000..27568961 --- /dev/null +++ b/core/src/main/kotlin/ch/kleis/lcaac/core/datasource/CsvSourceOperations.kt @@ -0,0 +1,108 @@ +package ch.kleis.lcaac.core.datasource + +import ch.kleis.lcaac.core.lang.evaluator.EvaluatorException +import ch.kleis.lcaac.core.lang.evaluator.reducer.DataExpressionReducer +import ch.kleis.lcaac.core.lang.expression.* +import ch.kleis.lcaac.core.lang.register.DataSourceRegister +import ch.kleis.lcaac.core.math.QuantityOperations +import ch.kleis.lcaac.core.math.basic.BasicNumber +import ch.kleis.lcaac.core.math.basic.BasicOperations +import ch.kleis.lcaac.core.prelude.Prelude +import org.apache.commons.csv.CSVFormat +import org.apache.commons.csv.CSVParser +import java.io.File +import java.lang.Double.parseDouble +import java.nio.file.Paths + +class CsvSourceOperations( + private val path: File, + private val ops: QuantityOperations, +) : DataSourceOperations { + private val format = CSVFormat.DEFAULT.builder() + .setHeader() + .setSkipHeaderRecord(true) + .build() + + override fun readAll(source: DataSourceExpression): Sequence> { + return when (source) { + is ECsvSource -> { + val location = Paths.get(path.absolutePath, source.location) + val inputStream = location.toFile().inputStream() + val parser = CSVParser(inputStream.reader(), format) + val header = parser.headerMap + parser.iterator().asSequence() + .map { record -> + val entries = header + .filter { entry -> source.schema.containsKey(entry.key) } + .mapValues { entry -> + val columnType = source.schema[entry.key]!! + val columnDefaultValue = columnType.defaultValue + val position = entry.value + val element = record[position] + when (columnDefaultValue) { + is QuantityExpression<*> -> + parseQuantityWithDefaultUnit(element, EUnitOf(columnDefaultValue)) + + is StringExpression -> + EStringLiteral(element) + + else -> throw IllegalStateException( + "invalid schema: column '${entry.key}' has an invalid default value" + ) + } + } + ERecord(entries) + } + } + } + } + + override fun sumProduct( + source: DataSourceExpression, + columns: List, + ): DataExpression { + val reducer = DataExpressionReducer( + dataRegister = Prelude.units(), + dataSourceRegister = DataSourceRegister.empty(), + ops = ops, + sourceOps = this, + ) + return when (source) { + is ECsvSource -> { + val location = Paths.get(path.absolutePath, source.location) + val inputStream = location.toFile().inputStream() + val parser = CSVParser(inputStream.reader(), format) + val header = parser.headerMap + parser.iterator().asSequence() + .map { record -> + columns.map { column -> + val position = header[column] + ?: throw IllegalStateException( + "${source.location}: invalid schema: unknown column '$column'" + ) + val columnType = source.schema[column] + ?: throw IllegalStateException( + "invalid schema: column '$column' has an invalid default value" + ) + val defaultValue = columnType.defaultValue + val element = record[position] + parseQuantityWithDefaultUnit(element, EUnitOf(defaultValue)) + }.reduce { acc, expression -> + reducer.reduce(EQuantityMul(acc, expression)) + } + }.reduce { acc, expression -> + reducer.reduce(EQuantityAdd(acc, expression)) + } + } + } + } + private fun parseQuantityWithDefaultUnit(s: String, defaultUnit: DataExpression): + DataExpression { + val amount = try { + parseDouble(s) + } catch (e: NumberFormatException) { + throw EvaluatorException("'$s' is not a valid number") + } + return EQuantityScale(ops.pure(amount), defaultUnit) + } +} diff --git a/core/src/main/kotlin/ch/kleis/lcaac/core/datasource/DataSourceOperations.kt b/core/src/main/kotlin/ch/kleis/lcaac/core/datasource/DataSourceOperations.kt new file mode 100644 index 00000000..df937b56 --- /dev/null +++ b/core/src/main/kotlin/ch/kleis/lcaac/core/datasource/DataSourceOperations.kt @@ -0,0 +1,10 @@ +package ch.kleis.lcaac.core.datasource + +import ch.kleis.lcaac.core.lang.expression.DataExpression +import ch.kleis.lcaac.core.lang.expression.DataSourceExpression +import ch.kleis.lcaac.core.lang.expression.ERecord + +interface DataSourceOperations { + fun readAll(source: DataSourceExpression): Sequence> + fun sumProduct(source: DataSourceExpression, columns: List): DataExpression +} diff --git a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/SymbolTable.kt b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/SymbolTable.kt index 566f9e5c..c554006e 100644 --- a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/SymbolTable.kt +++ b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/SymbolTable.kt @@ -6,10 +6,11 @@ import ch.kleis.lcaac.core.lang.register.* data class SymbolTable( - val data: DataRegister = DataRegister.empty(), - val dimensions: DimensionRegister = DimensionRegister.empty(), - val processTemplates: ProcessTemplateRegister = ProcessTemplateRegister.empty(), - val substanceCharacterizations: SubstanceCharacterizationRegister = SubstanceCharacterizationRegister.empty(), + val data: DataRegister = DataRegister.empty(), + val dimensions: DimensionRegister = DimensionRegister.empty(), + val processTemplates: ProcessTemplateRegister = ProcessTemplateRegister.empty(), + val substanceCharacterizations: SubstanceCharacterizationRegister = SubstanceCharacterizationRegister.empty(), + val dataSources: DataSourceRegister = DataSourceRegister.empty(), ) { companion object { fun empty() = SymbolTable() @@ -26,11 +27,11 @@ data class SymbolTable( */ private val templatesIndexedByProductName: Index> = Index( - processTemplates, - EProcessTemplate.body().products() compose - Every.list() compose - ETechnoExchange.product() compose - EProductSpec.name() + processTemplates, + EProcessTemplate.body().products() compose + Every.list() compose + ETechnoExchange.product() compose + EProductSpec.name() ) fun getTemplate(name: String): EProcessTemplate? { @@ -50,10 +51,10 @@ data class SymbolTable( Substances */ fun getSubstanceCharacterization( - name: String, - type: SubstanceType, - compartment: String, - subCompartment: String? = null, + name: String, + type: SubstanceType, + compartment: String, + subCompartment: String? = null, ): ESubstanceCharacterization? { return substanceCharacterizations[SubstanceKey(name, type, compartment, subCompartment)] } @@ -65,5 +66,9 @@ data class SymbolTable( return data[DataKey(name)] } + fun getDataSource(name: String): DataSourceExpression? { + return dataSources[DataSourceKey(name)] + } + } 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 07ad1528..9e1d0381 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 @@ -1,5 +1,6 @@ package ch.kleis.lcaac.core.lang.evaluator +import ch.kleis.lcaac.core.datasource.DataSourceOperations import ch.kleis.lcaac.core.lang.SymbolTable import ch.kleis.lcaac.core.lang.evaluator.protocol.Learner import ch.kleis.lcaac.core.lang.evaluator.protocol.Oracle @@ -11,13 +12,14 @@ import org.slf4j.LoggerFactory class Evaluator( private val symbolTable: SymbolTable, private val ops: QuantityOperations, + private val sourceOps: DataSourceOperations, ) { @Suppress("PrivatePropertyName") private val LOG = LoggerFactory.getLogger(Evaluator::class.java) fun trace(initialRequests: Set>): EvaluationTrace { val learner = Learner(initialRequests, ops) - val oracle = Oracle(symbolTable, ops) + val oracle = Oracle(symbolTable, ops, sourceOps) LOG.info("Start evaluation") try { var requests = learner.start() @@ -41,7 +43,7 @@ class Evaluator( mapOf(processKey to template) ) ) - return Evaluator(st, ops) + return Evaluator(st, ops, sourceOps) } fun trace( diff --git a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/ToValue.kt b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/ToValue.kt index 11c405a7..25b85e02 100644 --- a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/ToValue.kt +++ b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/ToValue.kt @@ -16,12 +16,19 @@ class ToValue( this.name, this.labels.mapValues { it.value.toValue() as StringValue }, this.products.map { it.toValue() }, - this.inputs.map { it.toValue() }, - this.biosphere.map { it.toValue() }, - this.impacts.map { it.toValue() } + this.inputs.flatMap { blockToValue(it) { block -> block.toValue() } }, + this.biosphere.flatMap { blockToValue(it) { block -> block.toValue() } }, + this.impacts.flatMap { blockToValue(it) { block -> block.toValue() } }, ) } + private fun blockToValue(block: BlockExpression, map: (E) -> V): List { + return when(block) { + is EBlockEntry -> listOf(map(block.entry)) + is EBlockForEach -> throw EvaluatorException("block $block is not reduced") + } + } + fun DataExpression.toValue(): DataValue { return when (this) { is EStringLiteral -> StringValue(this.value) @@ -62,6 +69,7 @@ class ToValue( fun EProductSpec.toValue(): ProductValue { val name = this.name + @Suppress("UNCHECKED_CAST") val referenceUnitValue = (this.referenceUnit as QuantityExpression?) ?.toUnitValue() @@ -124,7 +132,7 @@ class ToValue( fun ESubstanceCharacterization.toValue(): SubstanceCharacterizationValue { return SubstanceCharacterizationValue( referenceExchange = this.referenceExchange.toValue(), - impacts = this.impacts.map { it.toValue() }, + impacts = this.impacts.flatMap { blockToValue(it) { block -> block.toValue() } }, ) } @@ -136,7 +144,9 @@ class ToValue( when (val e = it.value) { is QuantityExpression<*> -> e.toValue() is StringExpression -> e.toValue() - is EDataRef -> throw EvaluatorException("$it is not reduced") + is ERecord -> RecordValue(e.entries.mapValues { it.value.toValue() }) + is EDataRef, is ERecordEntry, + is EDefaultRecordOf, is ESumProduct -> throw EvaluatorException("$it is not reduced") } }, ) diff --git a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/protocol/Learner.kt b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/protocol/Learner.kt index 3a2db6df..28f10d0c 100644 --- a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/protocol/Learner.kt +++ b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/protocol/Learner.kt @@ -1,8 +1,11 @@ package ch.kleis.lcaac.core.lang.evaluator.protocol +import arrow.optics.Every import ch.kleis.lcaac.core.lang.evaluator.EvaluationTrace +import ch.kleis.lcaac.core.lang.evaluator.EvaluatorException import ch.kleis.lcaac.core.lang.evaluator.ToValue import ch.kleis.lcaac.core.lang.expression.* +import ch.kleis.lcaac.core.lang.expression.optics.everyEntry import ch.kleis.lcaac.core.math.QuantityOperations /* @@ -58,18 +61,24 @@ class Learner( } private fun applyKnowledge(process: EProcess): EProcess { - return process.copy( - inputs = process.inputs.map { exchange -> - exchange.copy( - product = knowledge[exchange.product] as EProductSpec? ?: exchange.product - ) - }, - biosphere = process.biosphere.map { exchange -> - exchange.copy( - substance = knowledge[exchange.substance] as ESubstanceSpec? ?: exchange.substance - ) + val everyInputProductSpec = EProcess.inputs() compose + Every.list() compose + BlockExpression.everyEntry() compose + ETechnoExchange.product() + val everySubstance = EProcess.biosphere() compose + Every.list() compose + BlockExpression.everyEntry() compose + EBioExchange.substance() + return process + .let { p -> + everyInputProductSpec.modify(p) { + knowledge[it] as EProductSpec? ?: it + } + }.let { p -> + everySubstance.modify(p) { + knowledge[it] as ESubstanceSpec? ?: it + } } - ) } private fun applyKnowledge(substanceCharacterization: ESubstanceCharacterization): ESubstanceCharacterization { @@ -91,7 +100,7 @@ class Learner( private fun nextRequests(pairs: Set, Int>>): Set> = pairs.flatMap { (connection, index) -> - when(connection) { + when (connection) { is EProcess -> next(connection, index) is ESubstanceCharacterization -> emptySet() } @@ -132,7 +141,9 @@ class Learner( staging.find(address.connectionIndex) ?.let { existingConnection -> if (existingConnection is EProcess) { - knowledge[existingConnection.inputs[address.portIndex].product] = product + val block = existingConnection.inputs[address.portIndex] + if (block !is ETechnoBlockEntry) throw EvaluatorException("$block is not reduced") + knowledge[block.entry.product] = product } } staging.modify( @@ -146,11 +157,13 @@ class Learner( private fun next(process: EProcess, connectionIndex: Int): Set> { val productRequests = process.inputs.mapIndexed { portIndex, it -> - ProductRequest(Address(connectionIndex, portIndex), it.product) + if (it !is ETechnoBlockEntry) throw EvaluatorException("$it is not reduced") + ProductRequest(Address(connectionIndex, portIndex), it.entry.product) }.toSet() val substanceRequest = process.biosphere.mapIndexed { portIndex, it -> - SubstanceRequest(Address(connectionIndex, portIndex), it.substance) + if (it !is EBioBlockEntry) throw EvaluatorException("$it is not reduced") + SubstanceRequest(Address(connectionIndex, portIndex), it.entry.substance) }.toSet() return productRequests + substanceRequest @@ -170,7 +183,9 @@ class Learner( staging.find(address.connectionIndex) ?.let { existingConnection -> if (existingConnection is EProcess) { - knowledge[existingConnection.biosphere[address.portIndex].substance] = substance + val block = existingConnection.biosphere[address.portIndex] + if (block !is EBioBlockEntry) throw EvaluatorException("$block is not reduced") + knowledge[block.entry.substance] = substance } } staging.modify( @@ -188,11 +203,14 @@ class Learner( return { when (it) { is EProcess -> it.copy( - inputs = it.inputs.mapIndexed { index, exchange -> - if (index == portIndex) exchange.copy( - product = product + inputs = it.inputs.mapIndexed { index, block -> + if (block !is ETechnoBlockEntry) throw EvaluatorException("$block is not reduced") + if (index == portIndex) ETechnoBlockEntry( + entry = block.entry.copy( + product = product + ) ) - else exchange + else block } ) @@ -208,11 +226,14 @@ class Learner( return { when (it) { is EProcess -> it.copy( - biosphere = it.biosphere.mapIndexed { index, exchange -> - if (index == portIndex) exchange.copy( - substance = substance + biosphere = it.biosphere.mapIndexed { index, block -> + if (block !is EBioBlockEntry) throw EvaluatorException("$block is not reduced") + if (index == portIndex) EBioBlockEntry( + entry = block.entry.copy( + substance = substance + ) ) - else exchange + else block } ) 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 38c7c9c1..4147bfb3 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 @@ -1,5 +1,6 @@ package ch.kleis.lcaac.core.lang.evaluator.protocol +import ch.kleis.lcaac.core.datasource.DataSourceOperations 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 @@ -13,9 +14,10 @@ import ch.kleis.lcaac.core.math.QuantityOperations class Oracle( val symbolTable: SymbolTable, val ops: QuantityOperations, + private val sourceOps: DataSourceOperations, ) { - private val reduceLabelSelectors = ReduceLabelSelectors(symbolTable, ops) - private val reduceDataExpressions = Reduce(symbolTable, ops) + private val reduceLabelSelectors = ReduceLabelSelectors(symbolTable, ops, sourceOps) + private val reduceDataExpressions = Reduce(symbolTable, ops, sourceOps) private val completeTerminals = CompleteTerminals(ops) private val processResolver = ProcessResolver(symbolTable) private val substanceCharacterizationResolver = SubstanceCharacterizationResolver(symbolTable) diff --git a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/reducer/DataExpressionReducer.kt b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/reducer/DataExpressionReducer.kt index 0cb9bde4..aca12aa9 100644 --- a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/reducer/DataExpressionReducer.kt +++ b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/reducer/DataExpressionReducer.kt @@ -1,18 +1,24 @@ package ch.kleis.lcaac.core.lang.evaluator.reducer -import ch.kleis.lcaac.core.lang.register.DataKey -import ch.kleis.lcaac.core.lang.register.DataRegister +import ch.kleis.lcaac.core.datasource.DataSourceOperations 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.register.DataKey +import ch.kleis.lcaac.core.lang.register.DataRegister +import ch.kleis.lcaac.core.lang.register.DataSourceKey +import ch.kleis.lcaac.core.lang.register.DataSourceRegister import ch.kleis.lcaac.core.math.QuantityOperations import kotlin.math.pow class DataExpressionReducer( dataRegister: DataRegister, + dataSourceRegister: DataSourceRegister, private val ops: QuantityOperations, + private val sourceOps: DataSourceOperations, ) { private val dataRegister = DataRegister(dataRegister) + private val dataSourceRegister = DataSourceRegister(dataSourceRegister) private val infiniteUnitLoopChecker = InfiniteUnitLoopChecker() fun reduce(expression: DataExpression): DataExpression { @@ -30,10 +36,44 @@ class DataExpressionReducer( is EUnitLiteral -> EQuantityScale(pure(1.0), expression) is EUnitOf -> reduceUnitOf(expression) is EStringLiteral -> expression + is ERecord -> reduceMap(expression) + is ERecordEntry -> reduceMapEntry(expression) + is EDefaultRecordOf -> reduceDefaultRecordOf(expression) + is ESumProduct -> reduceESumProduct(expression) } } } + private fun reduceESumProduct(expression: ESumProduct): DataExpression { + val dataSource = dataSourceRegister[DataSourceKey(expression.dataSourceRef)] + ?: throw EvaluatorException("unknown data source '${expression.dataSourceRef}'") + return reduce(sourceOps.sumProduct(dataSource, expression.columns)) + } + + private fun reduceDefaultRecordOf(expression: EDefaultRecordOf): DataExpression { + val dataSource = dataSourceRegister[DataSourceKey(expression.dataSourceRef)] + ?: throw EvaluatorException("unknown data source '${expression.dataSourceRef}'") + val schema = dataSource.schema + return ERecord(schema.mapValues { reduce(it.value.defaultValue) }) + } + + private fun reduceMapEntry(expression: ERecordEntry): DataExpression { + return when (val map = reduce(expression.record)) { + is ERecord -> map.entries[expression.index] + ?: throw EvaluatorException("invalid index: '${expression.index}' not in ${map.entries.keys}") + + else -> ERecordEntry(map, expression.index) + } + } + + private fun reduceMap(expression: ERecord): DataExpression { + return ERecord( + expression.entries.mapValues { + reduce(it.value) + } + ) + } + private fun reduceUnitOf(unitOf: EUnitOf): DataExpression { with(ops) { @@ -68,7 +108,8 @@ class DataExpressionReducer( } private fun reduceClosure(closure: EQuantityClosure): DataExpression = - DataExpressionReducer(closure.symbolTable.data, ops).reduce(closure.expression) + DataExpressionReducer(closure.symbolTable.data, closure.symbolTable.dataSources, ops, sourceOps) + .reduce(closure.expression) private fun reduceDiv(expression: EQuantityDiv): DataExpression { with(ops) { @@ -170,10 +211,10 @@ class DataExpressionReducer( aliasForExpression is EQuantityScale && aliasForExpression.base is EUnitLiteral -> { EQuantityScale( pure(1.0), EUnitLiteral( - UnitSymbol.of(expression.symbol), - aliasForExpression.scale.toDouble() * aliasForExpression.base.scale, - aliasForExpression.base.dimension - ) + UnitSymbol.of(expression.symbol), + aliasForExpression.scale.toDouble() * aliasForExpression.base.scale, + aliasForExpression.base.dimension + ) ) } diff --git a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/reducer/LcaExpressionReducer.kt b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/reducer/LcaExpressionReducer.kt index 5407f034..dfc9f414 100644 --- a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/reducer/LcaExpressionReducer.kt +++ b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/reducer/LcaExpressionReducer.kt @@ -1,14 +1,27 @@ package ch.kleis.lcaac.core.lang.evaluator.reducer -import ch.kleis.lcaac.core.lang.register.DataRegister +import ch.kleis.lcaac.core.datasource.DataSourceOperations +import ch.kleis.lcaac.core.lang.evaluator.EvaluatorException import ch.kleis.lcaac.core.lang.expression.* +import ch.kleis.lcaac.core.lang.register.DataKey +import ch.kleis.lcaac.core.lang.register.DataRegister +import ch.kleis.lcaac.core.lang.register.DataSourceKey +import ch.kleis.lcaac.core.lang.register.DataSourceRegister import ch.kleis.lcaac.core.math.QuantityOperations class LcaExpressionReducer( - dataRegister: DataRegister = DataRegister.empty(), - ops: QuantityOperations, + private val dataRegister: DataRegister = DataRegister.empty(), + private val dataSourceRegister: DataSourceRegister = DataSourceRegister.empty(), + private val ops: QuantityOperations, + private val sourceOps: DataSourceOperations, ) { - private val dataExpressionReducer = DataExpressionReducer(dataRegister, ops) + private val dataExpressionReducer = DataExpressionReducer(dataRegister, dataSourceRegister, ops, sourceOps) + + private fun push(data: Map>): LcaExpressionReducer { + val localRegister = DataRegister(dataRegister) + .plus(data) + return LcaExpressionReducer(localRegister, dataSourceRegister, ops, sourceOps) + } fun reduce(expression: LcaExpression): LcaExpression { return when (expression) { @@ -28,7 +41,7 @@ class LcaExpressionReducer( fun reduceSubstanceCharacterization(expression: ESubstanceCharacterization): ESubstanceCharacterization { return ESubstanceCharacterization( reduceBioExchange(expression.referenceExchange), - expression.impacts.map(::reduceImpact), + expression.impacts.flatMap { reduceImpactBlock(it) }, ) } @@ -38,9 +51,9 @@ class LcaExpressionReducer( expression.name, expression.labels, expression.products.map(::reduceTechnoExchange), - expression.inputs.map(::reduceTechnoExchange), - expression.biosphere.map(::reduceBioExchange), - expression.impacts.map(::reduceImpact) + expression.inputs.flatMap { reduceTechnoBlock(it) }, + expression.biosphere.flatMap { reduceBioBlock(it) }, + expression.impacts.flatMap { reduceImpactBlock(it) }, ) } @@ -62,6 +75,80 @@ class LcaExpressionReducer( ) } + private fun reduceTechnoBlock(expression: TechnoBlock): + List> { + return when (expression) { + is EBlockEntry -> listOf( + ETechnoBlockEntry( + reduceTechnoExchange(expression.entry) + ) + ) + + is EBlockForEach -> { + val ds = dataSourceRegister[DataSourceKey(expression.dataSourceRef)] + ?: throw EvaluatorException("unknown data source '${expression.dataSourceRef}'") + sourceOps.readAll(ds) + .flatMap { record -> + val reducer = push(mapOf( + DataKey(expression.rowRef) to record + )).push( + expression.locals.mapKeys { DataKey(it.key) } + ) + expression.body + .flatMap { reducer.reduceTechnoBlock(it) } + }.toList() + } + } + } + + private fun reduceBioBlock(expression: BioBlock): + List> { + return when (expression) { + is EBlockEntry -> listOf( + EBioBlockEntry( + reduceBioExchange(expression.entry) + ) + ) + + is EBlockForEach -> { + val ds = dataSourceRegister[DataSourceKey(expression.dataSourceRef)] + ?: throw EvaluatorException("unknown data source '${expression.dataSourceRef}'") + sourceOps.readAll(ds) + .flatMap { record -> + val reducer = push(mapOf( + DataKey(expression.rowRef) to record + )) + expression.body + .flatMap { reducer.reduceBioBlock(it) } + }.toList() + } + } + } + + private fun reduceImpactBlock(expression: ImpactBlock): + List> { + return when (expression) { + is EBlockEntry -> listOf( + EImpactBlockEntry( + reduceImpact(expression.entry) + ) + ) + + is EBlockForEach -> { + val ds = dataSourceRegister[DataSourceKey(expression.dataSourceRef)] + ?: throw EvaluatorException("unknown data source '${expression.dataSourceRef}'") + sourceOps.readAll(ds) + .flatMap { record -> + val reducer = push(mapOf( + DataKey(expression.rowRef) to record + )) + expression.body + .flatMap { reducer.reduceImpactBlock(it) } + }.toList() + } + } + } + private fun reduceProductSpec(expression: EProductSpec): EProductSpec { return EProductSpec( expression.name, diff --git a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/reducer/TemplateExpressionReducer.kt b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/reducer/TemplateExpressionReducer.kt index 04a71c9a..fe88a089 100644 --- a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/reducer/TemplateExpressionReducer.kt +++ b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/reducer/TemplateExpressionReducer.kt @@ -1,19 +1,24 @@ package ch.kleis.lcaac.core.lang.evaluator.reducer import arrow.optics.Every +import ch.kleis.lcaac.core.datasource.DataSourceOperations import ch.kleis.lcaac.core.lang.register.DataKey import ch.kleis.lcaac.core.lang.register.DataRegister import ch.kleis.lcaac.core.lang.register.Register import ch.kleis.lcaac.core.lang.evaluator.EvaluatorException import ch.kleis.lcaac.core.lang.evaluator.Helper import ch.kleis.lcaac.core.lang.expression.* +import ch.kleis.lcaac.core.lang.register.DataSourceRegister import ch.kleis.lcaac.core.math.QuantityOperations class TemplateExpressionReducer( private val ops: QuantityOperations, + private val sourceOps: DataSourceOperations, dataRegister: DataRegister = DataRegister.empty(), + dataSourceRegister: DataSourceRegister = DataSourceRegister.empty(), ) { private val dataRegister = Register(dataRegister) + private val dataSourceRegister = Register(dataSourceRegister) private val helper = Helper() fun reduce(expression: EProcessTemplateApplication): EProcess { @@ -32,8 +37,8 @@ class TemplateExpressionReducer( .plus(actualArguments.mapKeys { DataKey(it.key) }) .plus(template.locals.mapKeys { DataKey(it.key) }) - val reducer = LcaExpressionReducer(localRegister, ops) - val dataReducer = DataExpressionReducer(localRegister, ops) + val reducer = LcaExpressionReducer(localRegister, dataSourceRegister, ops, sourceOps) + val dataReducer = DataExpressionReducer(localRegister, dataSourceRegister, ops, sourceOps) var result = template.body actualArguments.forEach { diff --git a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/step/CompleteTerminals.kt b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/step/CompleteTerminals.kt index 2334b2e2..07597707 100644 --- a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/step/CompleteTerminals.kt +++ b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/step/CompleteTerminals.kt @@ -3,6 +3,7 @@ package ch.kleis.lcaac.core.lang.evaluator.step import arrow.optics.Every import ch.kleis.lcaac.core.lang.evaluator.EvaluatorException import ch.kleis.lcaac.core.lang.expression.* +import ch.kleis.lcaac.core.lang.expression.optics.everyEntry import ch.kleis.lcaac.core.math.QuantityOperations class CompleteTerminals( @@ -10,7 +11,8 @@ class CompleteTerminals( ) { private val everyInputExchange = EProcess.inputs() compose - Every.list() + Every.list() compose + TechnoBlock.everyEntry() fun apply(expression: EProcess): EProcess = expression @@ -34,7 +36,11 @@ class CompleteTerminals( } private fun EProcess.completeSubstances(): EProcess { - return (EProcess.biosphere() compose Every.list()) + val everyBioExchange = + EProcess.biosphere() compose + Every.list() compose + BioBlock.everyEntry() + return everyBioExchange .modify(this) { exchange -> val referenceUnit = exchangeReferenceUnit(exchange) @@ -47,8 +53,11 @@ class CompleteTerminals( } } - private fun completeIndicators(impacts: Collection>): List> = - impacts.map { exchange -> + private fun completeIndicators(impacts: List>): List> { + val everyImpact = + Every.list>() compose + ImpactBlock.everyEntry() + return everyImpact.modify(impacts) { exchange -> val referenceUnit = exchangeReferenceUnit(exchange) EImpact.indicator() @@ -56,10 +65,11 @@ class CompleteTerminals( EIndicatorSpec(it.name, referenceUnit) } } + } private fun EProcess.completeProcessIndicators(): EProcess = this.copy( - impacts = completeIndicators(this.impacts) + impacts = completeIndicators(this.impacts) ) private fun ESubstanceCharacterization.completeSubstanceIndicators(): ESubstanceCharacterization = diff --git a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/step/Reduce.kt b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/step/Reduce.kt index 86a82433..98336186 100644 --- a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/step/Reduce.kt +++ b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/step/Reduce.kt @@ -1,5 +1,6 @@ package ch.kleis.lcaac.core.lang.evaluator.step +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.evaluator.Helper @@ -13,14 +14,19 @@ import ch.kleis.lcaac.core.math.QuantityOperations class Reduce( symbolTable: SymbolTable, ops: QuantityOperations, + sourceOps: DataSourceOperations, ) { private val lcaReducer = LcaExpressionReducer( symbolTable.data, - ops + symbolTable.dataSources, + ops, + sourceOps, ) private val templateReducer = TemplateExpressionReducer( ops, + sourceOps, symbolTable.data, + symbolTable.dataSources, ) fun apply(expression: EProcessTemplateApplication): EProcess { diff --git a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/step/ReduceLabelSelectors.kt b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/step/ReduceLabelSelectors.kt index cc52b43c..62405eec 100644 --- a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/step/ReduceLabelSelectors.kt +++ b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/step/ReduceLabelSelectors.kt @@ -1,21 +1,25 @@ package ch.kleis.lcaac.core.lang.evaluator.step import arrow.optics.Every -import ch.kleis.lcaac.core.lang.register.DataKey -import ch.kleis.lcaac.core.lang.register.Register +import ch.kleis.lcaac.core.datasource.DataSourceOperations import ch.kleis.lcaac.core.lang.SymbolTable import ch.kleis.lcaac.core.lang.evaluator.reducer.DataExpressionReducer import ch.kleis.lcaac.core.lang.expression.* import ch.kleis.lcaac.core.lang.expression.optics.everyDataRefInDataExpression +import ch.kleis.lcaac.core.lang.expression.optics.everyEntry +import ch.kleis.lcaac.core.lang.register.DataKey +import ch.kleis.lcaac.core.lang.register.Register import ch.kleis.lcaac.core.math.QuantityOperations class ReduceLabelSelectors( private val symbolTable: SymbolTable, private val ops: QuantityOperations, + private val sourceOps: DataSourceOperations, ) { private val everyInputProduct = EProcessTemplateApplication.template().body().inputs() compose Every.list() compose + BlockExpression.everyEntry() compose ETechnoExchange.product() private val everyLabelSelector = everyInputProduct compose EProductSpec.fromProcess().matchLabels().elements() compose @@ -32,7 +36,9 @@ class ReduceLabelSelectors( .plus(actualArguments.mapKeys { DataKey(it.key) }) .plus(labels.mapKeys { DataKey(it.key) }) .plus(locals.mapKeys { DataKey(it.key) }), + symbolTable.dataSources, ops, + sourceOps, ) return everyLabelSelector.modify(expression) { ref -> reducer.reduce(ref) } } diff --git a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/expression/DataExpression.kt b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/expression/DataExpression.kt index 03713bb2..a1a2bc79 100644 --- a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/expression/DataExpression.kt +++ b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/expression/DataExpression.kt @@ -13,6 +13,41 @@ sealed interface DataExpression { sealed interface QuantityExpression sealed interface StringExpression +/* + Record + */ + +@optics +data class EDefaultRecordOf(val dataSourceRef: String) : DataExpression { + companion object +} + +@optics +data class ERecord(val entries: Map>) : DataExpression { + companion object +} + +@optics +data class ERecordEntry(val record: DataExpression, val index: String) : DataExpression { + companion object +} + +/* + Column operations + */ + +sealed interface ColumnOperationExpression + +@optics +data class ESumProduct(val dataSourceRef: String, val columns: List) + : DataExpression, ColumnOperationExpression { + companion object +} + +/* + Ref + */ + @optics data class EDataRef(val name: String) : DataExpression { fun name(): String { diff --git a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/expression/DataSourceExpression.kt b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/expression/DataSourceExpression.kt new file mode 100644 index 00000000..5f2f9d66 --- /dev/null +++ b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/expression/DataSourceExpression.kt @@ -0,0 +1,15 @@ +package ch.kleis.lcaac.core.lang.expression + +data class ColumnType( + val defaultValue: DataExpression +) + +sealed interface DataSourceExpression { + val schema: Map> +} + +data class ECsvSource( + val location: String, + override val schema: Map>, +) : DataSourceExpression + diff --git a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/expression/LcaExpression.kt b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/expression/LcaExpression.kt index a647183d..7fc90cab 100644 --- a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/expression/LcaExpression.kt +++ b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/expression/LcaExpression.kt @@ -88,15 +88,48 @@ data class EImpact(override val quantity: DataExpression, val indicator: E companion object } +// Block +@optics +sealed interface BlockExpression { + companion object +} + +@optics +data class EBlockEntry(val entry: E) : BlockExpression { + companion object +} + +@optics +data class EBlockForEach( + val rowRef: String, + val dataSourceRef: String, + val locals: Map>, + val body: List>, +) : BlockExpression { + companion object +} + +typealias TechnoBlock = BlockExpression, Q> +typealias ETechnoBlockEntry = EBlockEntry, Q> +typealias ETechnoBlockForEach = EBlockForEach, Q> + +typealias BioBlock = BlockExpression, Q> +typealias EBioBlockEntry = EBlockEntry, Q> +typealias EBioBlockForEach = EBlockForEach, Q> + +typealias ImpactBlock = BlockExpression, Q> +typealias EImpactBlockEntry = EBlockEntry, Q> +typealias EImpactBlockForEach = EBlockForEach, Q> + // Process @optics data class EProcess( val name: String, val labels: Map> = emptyMap(), val products: List> = emptyList(), - val inputs: List> = emptyList(), - val biosphere: List> = emptyList(), - val impacts: List> = emptyList(), + val inputs: List> = emptyList(), + val biosphere: List> = emptyList(), + val impacts: List> = emptyList(), ) : LcaExpression, ConnectionExpression { companion object } @@ -105,7 +138,7 @@ data class EProcess( @optics data class ESubstanceCharacterization( val referenceExchange: EBioExchange, - val impacts: List> + val impacts: List> ) : LcaExpression, ConnectionExpression { companion object diff --git a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/expression/optics/EveryDataRef.kt b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/expression/optics/EveryDataRef.kt index 06a35ff9..db9f4c96 100644 --- a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/expression/optics/EveryDataRef.kt +++ b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/expression/optics/EveryDataRef.kt @@ -47,6 +47,13 @@ fun everyDataRefInDataExpression(): PEvery, DataExpression is EUnitLiteral -> M.empty() is EUnitOf -> foldMap(M, source.expression, map) is EStringLiteral -> M.empty() + is ERecord -> M.fold( + source.entries.values + .map { foldMap(M, it, map) } + ) + is ERecordEntry -> foldMap(M, source.record, map) + is EDefaultRecordOf -> M.empty() + is ESumProduct -> M.empty() } } @@ -102,6 +109,18 @@ fun everyDataRefInDataExpression(): PEvery, DataExpression ) is EStringLiteral -> source + is ERecord -> ERecord( + source.entries.mapValues { + modify(it.value, map) + } + ) + is ERecordEntry -> ERecordEntry( + modify(source.record, map), + source.index, + ) + + is EDefaultRecordOf -> source + is ESumProduct -> source } } } @@ -141,9 +160,15 @@ fun everyDataRefInProcess(): PEvery, EProcess, EDataRef, D Merge( listOf( EProcess.products() compose Every.list() compose everyDataRefInETechnoExchange(), - EProcess.inputs() compose Every.list() compose everyDataRefInETechnoExchange(), - EProcess.biosphere() compose Every.list() compose everyDataRefInEBioExchange(), - EProcess.impacts() compose Every.list() compose everyDataRefInEImpact(), + EProcess.inputs() compose Every.list() compose + BlockExpression.everyEntry() compose + everyDataRefInETechnoExchange(), + EProcess.biosphere() compose Every.list() compose + BlockExpression.everyEntry() compose + everyDataRefInEBioExchange(), + EProcess.impacts() compose Every.list() compose + BlockExpression.everyEntry() compose + everyDataRefInEImpact(), ) ) @@ -151,7 +176,9 @@ private fun everyDataRefInSubstanceCharacterization(): PEvery() compose everyDataRefInEBioExchange(), - ESubstanceCharacterization.impacts() compose Every.list() compose everyDataRefInEImpact(), + ESubstanceCharacterization.impacts() compose Every.list() compose + BlockExpression.everyEntry() compose + everyDataRefInEImpact(), ) ) @@ -172,15 +199,3 @@ fun everyDataRefInLcaExpression(): PEvery, LcaExpression everyDataRefInSubstanceCharacterization() ) ) - -fun everyDataRefInTemplateExpression(): PEvery, ProcessTemplateExpression, EDataRef, DataExpression> = - everyProcessTemplateInTemplateExpression() compose Merge( - listOf( - EProcessTemplate.params() compose Every.map() compose - everyDataRefInDataExpression(), - EProcessTemplate.locals() compose Every.map() compose - everyDataRefInDataExpression(), - EProcessTemplate.body() compose everyDataRefInProcess(), - ) - ) - diff --git a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/expression/optics/EveryProcessTemplate.kt b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/expression/optics/EveryProcessTemplate.kt deleted file mode 100644 index 064653f6..00000000 --- a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/expression/optics/EveryProcessTemplate.kt +++ /dev/null @@ -1,10 +0,0 @@ -package ch.kleis.lcaac.core.lang.expression.optics - -import ch.kleis.lcaac.core.lang.expression.* - -fun everyProcessTemplateInTemplateExpression() = Merge( - listOf( - ProcessTemplateExpression.eProcessTemplateApplication().template(), - ProcessTemplateExpression.eProcessTemplate(), - ) -) \ No newline at end of file diff --git a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/expression/optics/LcaExpressionOptics.kt b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/expression/optics/LcaExpressionOptics.kt index fc2b5d57..fecc859b 100644 --- a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/expression/optics/LcaExpressionOptics.kt +++ b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/expression/optics/LcaExpressionOptics.kt @@ -5,8 +5,35 @@ package ch.kleis.lcaac.core.lang.expression.optics import arrow.core.identity import arrow.core.left import arrow.core.right +import arrow.optics.Every +import arrow.typeclasses.Monoid import ch.kleis.lcaac.core.lang.expression.* +inline fun BlockExpression.Companion.everyEntry(): Every, E> = + object : Every, E> { + override fun foldMap(M: Monoid, source: BlockExpression, map: (focus: E) -> R): R { + return when(source) { + is EBlockEntry -> map(source.entry) + is EBlockForEach -> M.fold( + source.body.map { foldMap(M, it, map) } + ) + } + } + + override fun modify(source: BlockExpression, map: (focus: E) -> E): BlockExpression { + return when(source) { + is EBlockEntry -> EBlockEntry(map(source.entry)) + is EBlockForEach -> EBlockForEach( + source.rowRef, + source.dataSourceRef, + source.locals, + source.body.map { + modify(it, map) + } + ) + } + } + } inline fun LcaExpression.Companion.eProcess(): arrow.optics.Prism, EProcess> = arrow.optics.Prism( diff --git a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/register/DataSourceRegister.kt b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/register/DataSourceRegister.kt new file mode 100644 index 00000000..b97e6d96 --- /dev/null +++ b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/register/DataSourceRegister.kt @@ -0,0 +1,12 @@ +package ch.kleis.lcaac.core.lang.register + +import ch.kleis.lcaac.core.lang.expression.DataSourceExpression + +data class DataSourceKey( + val name: String, +) { + override fun toString(): String = name +} + +typealias DataSourceRegister = Register> + diff --git a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/type/Type.kt b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/type/Type.kt index 1e9c81bc..d2c0ecfc 100644 --- a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/type/Type.kt +++ b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/type/Type.kt @@ -7,14 +7,11 @@ sealed interface Type data class TUnit(val dimension: Dimension) : Type sealed interface TypeDataExpression : Type -object TString : TypeDataExpression { - override fun toString(): String { - return "TString" - } -} +data object TString : TypeDataExpression data class TQuantity(val dimension: Dimension) : TypeDataExpression +data class TRecord(val entries: Map) : TypeDataExpression sealed interface TypeLcaExpression : Type diff --git a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/value/DataValue.kt b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/value/DataValue.kt index 6927f1c9..4de7d444 100644 --- a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/value/DataValue.kt +++ b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/value/DataValue.kt @@ -13,3 +13,9 @@ data class QuantityValue(val amount: Q, val unit: UnitValue) : DataValue(val entries: Map>) : DataValue { + override fun toString(): String { + return entries.toString() + } +} 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 bf8f4a49..2a2565f5 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 @@ -1,6 +1,7 @@ package ch.kleis.lcaac.core.testing import ch.kleis.lcaac.core.assessment.ContributionAnalysisProgram +import ch.kleis.lcaac.core.datasource.DataSourceOperations import ch.kleis.lcaac.core.lang.SymbolTable import ch.kleis.lcaac.core.lang.evaluator.Evaluator import ch.kleis.lcaac.core.lang.evaluator.EvaluatorException @@ -14,36 +15,42 @@ import ch.kleis.lcaac.core.math.basic.BasicOperations class BasicTestRunner( symbolTable: SymbolTable, - private val evaluator: Evaluator = Evaluator(symbolTable, BasicOperations), - private val dataReducer: DataExpressionReducer = DataExpressionReducer(symbolTable.data, BasicOperations), + sourceOps: DataSourceOperations, + private val evaluator: Evaluator = Evaluator(symbolTable, BasicOperations, sourceOps), + private val dataReducer: DataExpressionReducer = DataExpressionReducer( + symbolTable.data, + symbolTable.dataSources, + BasicOperations, + sourceOps, + ), ) { fun run(case: TestCase): TestResult { try { - val trace = evaluator.with(case.template).trace(case.template) - val program = ContributionAnalysisProgram(trace.getSystemValue(), trace.getEntryPoint()) - val analysis = program.run() - val target = trace.getEntryPoint().products.first().port() - val results = case.assertions.map { assertion -> - val ports = analysis.findAllPortsByShortName(assertion.ref) - if (ports.isEmpty()) { - GenericFailure("unknown reference '${assertion.ref}'") - } else { - val impact = with(QuantityValueOperations(BasicOperations)) { - ports.map { - if (analysis.isControllable(it)) analysis.getPortContribution(target, it) - else analysis.supplyOf(it) - }.reduce { acc, quantityValue -> acc + quantityValue } + val trace = evaluator.with(case.template).trace(case.template) + val program = ContributionAnalysisProgram(trace.getSystemValue(), trace.getEntryPoint()) + val analysis = program.run() + val target = trace.getEntryPoint().products.first().port() + val results = case.assertions.map { assertion -> + val ports = analysis.findAllPortsByShortName(assertion.ref) + if (ports.isEmpty()) { + GenericFailure("unknown reference '${assertion.ref}'") + } else { + val impact = with(QuantityValueOperations(BasicOperations)) { + ports.map { + if (analysis.isControllable(it)) analysis.getPortContribution(target, it) + else analysis.supplyOf(it) + }.reduce { acc, quantityValue -> acc + quantityValue } + } + val lo = with(ToValue(BasicOperations)) { dataReducer.reduce(assertion.lo).toValue() } + val hi = with(ToValue(BasicOperations)) { dataReducer.reduce(assertion.hi).toValue() } + test(assertion.ref, impact, lo, hi) } - val lo = with(ToValue(BasicOperations)) { dataReducer.reduce(assertion.lo).toValue() } - val hi = with(ToValue(BasicOperations)) { dataReducer.reduce(assertion.hi).toValue() } - test(assertion.ref, impact, lo, hi) } - } - return TestResult( - case.source, - case.name, - results, - ) + return TestResult( + case.source, + case.name, + results, + ) } catch (e: EvaluatorException) { return TestResult( case.source, diff --git a/core/src/test/kotlin/ch/kleis/lcaac/core/lang/evaluator/EvaluatorTest.kt b/core/src/test/kotlin/ch/kleis/lcaac/core/lang/evaluator/EvaluatorTest.kt index f24610a6..358fa1d4 100644 --- a/core/src/test/kotlin/ch/kleis/lcaac/core/lang/evaluator/EvaluatorTest.kt +++ b/core/src/test/kotlin/ch/kleis/lcaac/core/lang/evaluator/EvaluatorTest.kt @@ -1,6 +1,7 @@ package ch.kleis.lcaac.core.lang.evaluator -import ch.kleis.lcaac.core.lang.* +import ch.kleis.lcaac.core.datasource.DataSourceOperations +import ch.kleis.lcaac.core.lang.SymbolTable import ch.kleis.lcaac.core.lang.dimension.UnitSymbol import ch.kleis.lcaac.core.lang.expression.* import ch.kleis.lcaac.core.lang.fixture.* @@ -11,6 +12,7 @@ import ch.kleis.lcaac.core.lang.register.SubstanceKey import ch.kleis.lcaac.core.lang.value.* import ch.kleis.lcaac.core.math.basic.BasicNumber import ch.kleis.lcaac.core.math.basic.BasicOperations +import io.mockk.mockk import org.junit.jupiter.api.Test import org.junit.jupiter.api.Timeout import org.junit.jupiter.api.assertThrows @@ -20,6 +22,7 @@ import kotlin.test.assertNotEquals class EvaluatorTest { private val ops = BasicOperations + private val sourceOps = mockk>() @Test fun with_whenNewTemplate() { @@ -31,11 +34,11 @@ class EvaluatorTest { ETechnoExchange(QuantityFixture.oneKilogram, ProductFixture.carrot) ), impacts = listOf( - ImpactFixture.oneClimateChange + ImpactBlockFixture.oneClimateChange ), ) ) - val evaluator = Evaluator(SymbolTable.empty(), BasicOperations) + val evaluator = Evaluator(SymbolTable.empty(), ops, sourceOps) .with(template) val expected = ImpactValue( QuantityValueFixture.oneKilogram, @@ -59,7 +62,7 @@ class EvaluatorTest { ETechnoExchange(QuantityFixture.oneKilogram, ProductFixture.carrot) ), impacts = listOf( - ImpactFixture.oneClimateChange + ImpactBlockFixture.oneClimateChange ), ) ) @@ -68,7 +71,7 @@ class EvaluatorTest { ProcessKey("eProcess") to template )) ) - val evaluator = Evaluator(symbolTable, BasicOperations) + val evaluator = Evaluator(symbolTable, ops, sourceOps) // when/then val e = assertThrows { evaluator.with(template) } @@ -85,7 +88,7 @@ class EvaluatorTest { ETechnoExchange(QuantityFixture.oneKilogram, ProductFixture.carrot) ), impacts = listOf( - ImpactFixture.oneClimateChange + ImpactBlockFixture.oneClimateChange ), ) ) @@ -96,7 +99,7 @@ class EvaluatorTest { ) ) ) - val evaluator = Evaluator(symbolTable, ops) + val evaluator = Evaluator(symbolTable, ops, sourceOps) val expected = ImpactValue( QuantityValueFixture.oneKilogram, IndicatorValueFixture.climateChange, @@ -119,14 +122,16 @@ class EvaluatorTest { ETechnoExchange(QuantityFixture.oneKilogram, EProductSpec("p", QuantityFixture.oneKilogram)) ), biosphere = listOf( - EBioExchange( - QuantityFixture.oneKilogram, - ESubstanceSpec( - "doesNotExist", - "doesNotExist", - SubstanceType.EMISSION, - "water", - "sea water" + EBioBlockEntry( + EBioExchange( + QuantityFixture.oneKilogram, + ESubstanceSpec( + "doesNotExist", + "doesNotExist", + SubstanceType.EMISSION, + "water", + "sea water" + ) ) ) ), @@ -139,7 +144,7 @@ class EvaluatorTest { ) ) ) - val evaluator = Evaluator(symbolTable, ops) + val evaluator = Evaluator(symbolTable, ops, sourceOps) val expected = FullyQualifiedSubstanceValue( "doesNotExist", type = SubstanceType.EMISSION, @@ -166,7 +171,7 @@ class EvaluatorTest { ) ) ) - val evaluator = Evaluator(symbolTable, ops) + val evaluator = Evaluator(symbolTable, ops, sourceOps) // when val p1 = evaluator.trace(template, mapOf("q_water" to QuantityFixture.oneLitre)) @@ -188,7 +193,7 @@ class EvaluatorTest { val register = ProcessTemplateRegister(mapOf(ProcessKey("carrot_production") to template)) val symbolTable = SymbolTable(processTemplates = register) - val evaluator = Evaluator(symbolTable, BasicOperations) + val evaluator = Evaluator(symbolTable, ops, sourceOps) // when @@ -211,10 +216,12 @@ class EvaluatorTest { ) ), inputs = listOf( - ETechnoExchange( - QuantityFixture.oneKilogram, - EProductSpec( - "carrot", + ETechnoBlockEntry( + ETechnoExchange( + QuantityFixture.oneKilogram, + EProductSpec( + "carrot", + ) ) ) ), @@ -228,7 +235,7 @@ class EvaluatorTest { ) ), ) - val evaluator = Evaluator(symbolTable, ops) + val evaluator = Evaluator(symbolTable, ops, sourceOps) // when val actual = evaluator.trace(template).getSystemValue().processes @@ -299,23 +306,25 @@ class EvaluatorTest { ) ), inputs = listOf( - ETechnoExchange( - QuantityFixture.oneKilogram, - EProductSpec( - "carrot", - UnitFixture.kg, - FromProcess( - "carrot_production", - MatchLabels(emptyMap()), - mapOf("q_water" to QuantityFixture.twoLitres), - ), + ETechnoBlockEntry( + ETechnoExchange( + QuantityFixture.oneKilogram, + EProductSpec( + "carrot", + UnitFixture.kg, + FromProcess( + "carrot_production", + MatchLabels(emptyMap()), + mapOf("q_water" to QuantityFixture.twoLitres), + ), + ) ) ) ), ), ) // given - val processTemplates= ProcessTemplateRegister( + val processTemplates = ProcessTemplateRegister( mapOf( ProcessKey("carrot_production") to TemplateFixture.carrotProduction, ProcessKey("salad_production") to template, @@ -324,7 +333,7 @@ class EvaluatorTest { val symbolTable = SymbolTable( processTemplates = processTemplates, ) - val evaluator = Evaluator(symbolTable, ops) + val evaluator = Evaluator(symbolTable, ops, sourceOps) // when val actual = evaluator.trace(template).getSystemValue().processes @@ -396,16 +405,18 @@ class EvaluatorTest { ) ), inputs = listOf( - ETechnoExchange( - QuantityFixture.oneKilogram, - EProductSpec( - "irrelevant_product", - UnitFixture.kg, - FromProcess( - "carrot_production", - MatchLabels(emptyMap()), - mapOf("q_water" to QuantityFixture.twoLitres), - ), + ETechnoBlockEntry( + ETechnoExchange( + QuantityFixture.oneKilogram, + EProductSpec( + "irrelevant_product", + UnitFixture.kg, + FromProcess( + "carrot_production", + MatchLabels(emptyMap()), + mapOf("q_water" to QuantityFixture.twoLitres), + ), + ) ) ) ), @@ -419,7 +430,7 @@ class EvaluatorTest { ).mapKeys { ProcessKey(it.key) } ) ) - val evaluator = Evaluator(symbolTable, ops) + val evaluator = Evaluator(symbolTable, ops, sourceOps) // when/then val e = assertFailsWith( @@ -441,12 +452,14 @@ class EvaluatorTest { ) ), biosphere = listOf( - EBioExchange( - QuantityFixture.oneKilogram, ESubstanceSpec( + EBioBlockEntry( + EBioExchange( + QuantityFixture.oneKilogram, ESubstanceSpec( "propanol", compartment = "air", type = SubstanceType.RESOURCE, ) + ) ) ), ) @@ -463,7 +476,7 @@ class EvaluatorTest { ) ) ) - val evaluator = Evaluator(symbolTable, ops) + val evaluator = Evaluator(symbolTable, ops, sourceOps) // when val actual = evaluator.trace(template).getSystemValue().substanceCharacterizations @@ -488,9 +501,11 @@ class EvaluatorTest { ) ), inputs = listOf( - ETechnoExchange( - QuantityFixture.oneLitre, - ProductFixture.carrot, + ETechnoBlockEntry( + ETechnoExchange( + QuantityFixture.oneLitre, + ProductFixture.carrot, + ) ) ), ) @@ -503,7 +518,7 @@ class EvaluatorTest { ).mapKeys { ProcessKey(it.key) } ) ) - val evaluator = Evaluator(symbolTable, ops) + val evaluator = Evaluator(symbolTable, ops, sourceOps) // when/then val e = assertFailsWith( diff --git a/core/src/test/kotlin/ch/kleis/lcaac/core/lang/evaluator/HelperTest.kt b/core/src/test/kotlin/ch/kleis/lcaac/core/lang/evaluator/HelperTest.kt index cebfc935..af0d6ac0 100644 --- a/core/src/test/kotlin/ch/kleis/lcaac/core/lang/evaluator/HelperTest.kt +++ b/core/src/test/kotlin/ch/kleis/lcaac/core/lang/evaluator/HelperTest.kt @@ -23,13 +23,15 @@ class HelperTest { ETechnoExchange(QuantityFixture.oneKilogram, ProductFixture.carrot) ), inputs = listOf( - ETechnoExchange( - QuantityFixture.oneKilogram, - ProductFixture.carrot.copy( - fromProcess = FromProcess( - name = "another_process", - matchLabels = MatchLabels(mapOf("class" to ref)), - ), + ETechnoBlockEntry( + ETechnoExchange( + QuantityFixture.oneKilogram, + ProductFixture.carrot.copy( + fromProcess = FromProcess( + name = "another_process", + matchLabels = MatchLabels(mapOf("class" to ref)), + ), + ) ) ) ), @@ -46,13 +48,15 @@ class HelperTest { ETechnoExchange(QuantityFixture.oneKilogram, ProductFixture.carrot) ), inputs = listOf( - ETechnoExchange( - QuantityFixture.oneKilogram, - ProductFixture.carrot.copy( - fromProcess = FromProcess( - name = "another_process", - matchLabels = MatchLabels(mapOf("class" to EStringLiteral("foo"))), - ), + ETechnoBlockEntry( + ETechnoExchange( + QuantityFixture.oneKilogram, + ProductFixture.carrot.copy( + fromProcess = FromProcess( + name = "another_process", + matchLabels = MatchLabels(mapOf("class" to EStringLiteral("foo"))), + ), + ) ) ) ), @@ -71,10 +75,10 @@ class HelperTest { ETechnoExchange(ref, ProductFixture.carrot) ), inputs = listOf( - ETechnoExchange(ref, ProductFixture.carrot) + ETechnoBlockEntry(ETechnoExchange(ref, ProductFixture.carrot)) ), biosphere = listOf( - EBioExchange(ref, SubstanceFixture.propanol) + EBioBlockEntry(EBioExchange(ref, SubstanceFixture.propanol)) ), ) val helper = Helper() @@ -89,10 +93,10 @@ class HelperTest { ETechnoExchange(QuantityFixture.oneKilogram, ProductFixture.carrot) ), inputs = listOf( - ETechnoExchange(QuantityFixture.oneKilogram, ProductFixture.carrot) + ETechnoBlockEntry(ETechnoExchange(QuantityFixture.oneKilogram, ProductFixture.carrot)) ), biosphere = listOf( - EBioExchange(QuantityFixture.oneKilogram, SubstanceFixture.propanol) + EBioBlockEntry(EBioExchange(QuantityFixture.oneKilogram, SubstanceFixture.propanol)) ), ) assertEquals(expected, actual) @@ -156,20 +160,24 @@ class HelperTest { name = "p", products = listOf(ETechnoExchange(EDataRef("quantity"), ProductFixture.carrot)), inputs = listOf( - ETechnoExchange( - EQuantityScale( - ops.pure(1.0), - EQuantityMul(EDataRef("ua"), EDataRef("ub")) - ), EProductSpec( + ETechnoBlockEntry( + ETechnoExchange( + EQuantityScale( + ops.pure(1.0), + EQuantityMul(EDataRef("ua"), EDataRef("ub")) + ), EProductSpec( "product", ) + ) ), - ETechnoExchange( - QuantityFixture.oneLitre, EProductSpec( + ETechnoBlockEntry( + ETechnoExchange( + QuantityFixture.oneLitre, EProductSpec( "water", UnitFixture.l, FromProcess(name = "template", matchLabels = MatchLabels(emptyMap())), ) + ) ), ), ) @@ -191,11 +199,13 @@ class HelperTest { val substanceCharacterization = ESubstanceCharacterization( EBioExchange(QuantityFixture.oneKilogram, ESubstanceSpec("substance")), listOf( - EImpact( - EQuantityAdd( - EQuantityScale(ops.pure(3.0), EDataRef("qa")), - EDataRef("qb") - ), EIndicatorSpec("indicator") + EImpactBlockEntry( + EImpact( + EQuantityAdd( + EQuantityScale(ops.pure(3.0), EDataRef("qa")), + EDataRef("qb") + ), EIndicatorSpec("indicator") + ) ) ), ) diff --git a/core/src/test/kotlin/ch/kleis/lcaac/core/lang/evaluator/reducer/DataExpressionReducerTest.kt b/core/src/test/kotlin/ch/kleis/lcaac/core/lang/evaluator/reducer/DataExpressionReducerTest.kt index e13370d4..5fa86638 100644 --- a/core/src/test/kotlin/ch/kleis/lcaac/core/lang/evaluator/reducer/DataExpressionReducerTest.kt +++ b/core/src/test/kotlin/ch/kleis/lcaac/core/lang/evaluator/reducer/DataExpressionReducerTest.kt @@ -1,8 +1,6 @@ package ch.kleis.lcaac.core.lang.evaluator.reducer -import ch.kleis.lcaac.core.lang.register.DataKey -import ch.kleis.lcaac.core.lang.register.DataRegister -import ch.kleis.lcaac.core.lang.register.Register +import ch.kleis.lcaac.core.datasource.DataSourceOperations import ch.kleis.lcaac.core.lang.SymbolTable import ch.kleis.lcaac.core.lang.dimension.Dimension import ch.kleis.lcaac.core.lang.dimension.UnitSymbol @@ -11,15 +9,246 @@ import ch.kleis.lcaac.core.lang.expression.* import ch.kleis.lcaac.core.lang.fixture.DimensionFixture import ch.kleis.lcaac.core.lang.fixture.QuantityFixture import ch.kleis.lcaac.core.lang.fixture.UnitFixture +import ch.kleis.lcaac.core.lang.register.* 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.verify import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows import kotlin.math.pow import kotlin.test.assertEquals import kotlin.test.assertFailsWith class DataExpressionReducerTest { private val ops = BasicOperations + private val sourceOps = mockk>() + + /* + Column Operations + */ + + @Test + fun sumProduct() { + // given + val expression = ESumProduct("source", listOf("volume", "mass")) + val dataSource = ECsvSource( + location = "source.csv", + schema = mapOf( + "volume" to ColumnType(QuantityFixture.oneLitre), + "mass" to ColumnType(QuantityFixture.oneKilogram), + ), + ) + val sops = mockk>() + val total = EQuantityScale(BasicNumber(1.0), + EUnitLiteral( + UnitFixture.l.symbol.multiply(UnitFixture.kg.symbol), + 1.0, + UnitFixture.l.dimension.multiply(UnitFixture.kg.dimension), + )) + every { sops.sumProduct(dataSource, listOf("volume", "mass")) } returns total + val reducer = DataExpressionReducer( + DataRegister.empty(), + DataSourceRegister.from(mapOf( + DataSourceKey("source") to dataSource + )), + ops, + sops, + ) + + // when + val actual = reducer.reduce(expression) + + // then + assertEquals(total, actual) + verify { sops.sumProduct(dataSource, listOf("volume", "mass")) } + } + + + @Test + fun sum_invalidDataSourceRef() { + // given + val expression = ESumProduct("foo", listOf("mass")) + val dataSource = ECsvSource( + location = "source.csv", + schema = mapOf( + "mass" to ColumnType(QuantityFixture.oneKilogram), + ), + ) + val sops = mockk>() + val total = QuantityFixture.twoKilograms + every { sops.sumProduct(dataSource, listOf("mass")) } returns total + val reducer = DataExpressionReducer( + DataRegister.empty(), + DataSourceRegister.from(mapOf( + DataSourceKey("source") to dataSource + )), + ops, + sops, + ) + + // when/then + val e = assertThrows { reducer.reduce(expression) } + assertEquals("unknown data source 'foo'", e.message) + } + + /* + RECORD + */ + @Test + fun reduce_whenDefaultRecordOfDataSourceRef() { + // given + val dataSource = ECsvSource( + location = "source.csv", + schema = mapOf( + "x" to ColumnType(EDataRef("a")), + ) + ) + val record = EDefaultRecordOf("source") + val reducer = DataExpressionReducer( + Register.from(mapOf( + DataKey("a") to QuantityFixture.oneKilogram, + )), + Register.from(mapOf( + DataSourceKey("source") to dataSource, + )), + ops, + sourceOps, + ) + + // when + val actual = reducer.reduce(record) + + // then + val expected = ERecord(mapOf( + "x" to QuantityFixture.oneKilogram, + )) + assertEquals(expected, actual) + } + + @Test + fun reduce_whenDefaultRecordOfDataSourceRef_invalidRef() { + // given + val dataSource = ECsvSource( + location = "source.csv", + schema = mapOf( + "x" to ColumnType(EDataRef("a")), + ) + ) + val record = EDefaultRecordOf("foo") + val reducer = DataExpressionReducer( + Register.from(mapOf( + DataKey("a") to QuantityFixture.oneKilogram, + )), + Register.from(mapOf( + DataSourceKey("source") to dataSource, + )), + ops, + sourceOps, + ) + + // when/then + val e = assertThrows { reducer.reduce(record) } + assertEquals("unknown data source 'foo'", e.message) + } + + @Test + fun reduce_whenRecord() { + // given + val q = QuantityFixture.oneKilogram + val record = ERecord(mapOf( + "mass" to EDataRef("m") + )) + val reducer = DataExpressionReducer( + Register.from(mapOf( + DataKey("m") to q + )), + Register.empty(), + ops, + sourceOps, + ) + + // when + val actual = reducer.reduce(record) + + // then + val expected = ERecord(mapOf( + "mass" to q + )) + assertEquals(expected, actual) + } + + @Test + fun reduce_whenRecordEntry_withRef() { + // given + val q = QuantityFixture.oneKilogram + val record = ERecord(mapOf( + "mass" to EDataRef("m") + )) + val entry = ERecordEntry(EDataRef("my_map"), "mass") + val reducer = DataExpressionReducer( + Register.from(mapOf( + DataKey("m") to q, + DataKey("my_map") to record, + )), + Register.empty(), + ops, + sourceOps, + ) + + // when + val actual = reducer.reduce(entry) + + // then + assertEquals(q, actual) + } + + @Test + fun reduce_whenRecordEntry() { + // given + val q = QuantityFixture.oneKilogram + val record = ERecord(mapOf( + "mass" to EDataRef("m") + )) + val entry = ERecordEntry(record, "mass") + val reducer = DataExpressionReducer( + Register.from(mapOf( + DataKey("m") to q + )), + Register.empty(), + ops, + sourceOps, + ) + + // when + val actual = reducer.reduce(entry) + + // then + assertEquals(q, actual) + } + + @Test + fun reduce_whenRecordEntry_invalidIndex() { + // given + val q = QuantityFixture.oneKilogram + val record = ERecord(mapOf( + "mass" to EDataRef("m") + )) + val entry = ERecordEntry(record, "foo") + val reducer = DataExpressionReducer( + Register.from(mapOf( + DataKey("m") to q + )), + Register.empty(), + ops, + sourceOps, + ) + + // when + val e = assertThrows { reducer.reduce(entry) } + assertEquals("invalid index: 'foo' not in [mass]", e.message) + } /* QUANTITIES @@ -30,8 +259,10 @@ class DataExpressionReducerTest { // given val unit = EUnitLiteral(UnitSymbol.of("a"), 123.0, Dimension.of("a")) val reducer = DataExpressionReducer( + Register.empty(), Register.empty(), ops, + sourceOps, ) // when @@ -48,8 +279,10 @@ class DataExpressionReducerTest { val innerQuantity = QuantityFixture.oneKilogram val quantity = EQuantityScale(ops.pure(2.0), innerQuantity) val reducer = DataExpressionReducer( + Register.empty(), Register.empty(), ops, + sourceOps, ) // when @@ -64,7 +297,7 @@ class DataExpressionReducerTest { fun test_reduce_whenScaleOfScale_shouldReduce() { // given val quantity = EQuantityScale(ops.pure(1.0), QuantityFixture.twoKilograms) - val reducer = DataExpressionReducer(Register.empty(), ops) + val reducer = DataExpressionReducer(Register.empty(), Register.empty(), ops, sourceOps) val expected = EQuantityScale(ops.pure(2.0), UnitFixture.kg) // when @@ -80,8 +313,10 @@ class DataExpressionReducerTest { val innerQuantity = EDataRef("a") val quantity = EQuantityScale(ops.pure(2.0), innerQuantity) val reducer = DataExpressionReducer( + Register.empty(), Register.empty(), ops, + sourceOps, ) // when @@ -100,7 +335,7 @@ class DataExpressionReducerTest { ) ) val quantity = EQuantityScale(ops.pure(1.0), EDataRef("kg")) - val reducer = DataExpressionReducer(quantityEnvironment, ops) + val reducer = DataExpressionReducer(quantityEnvironment, Register.empty(), ops, sourceOps) // when val actual = reducer.reduce(quantity) @@ -116,8 +351,10 @@ class DataExpressionReducerTest { val a = EQuantityScale(ops.pure(2.0), UnitFixture.kg) val b = EQuantityScale(ops.pure(1000.0), UnitFixture.g) val reducer = DataExpressionReducer( + Register.empty(), Register.empty(), ops, + sourceOps, ) // when @@ -134,7 +371,7 @@ class DataExpressionReducerTest { val a = UnitFixture.kg val b = UnitFixture.kg val expected = EQuantityScale(ops.pure(2.0), UnitFixture.kg) - val reducer = DataExpressionReducer(Register.empty(), ops) + val reducer = DataExpressionReducer(Register.empty(), Register.empty(), ops, sourceOps) // when val actual = reducer.reduce(EQuantityAdd(a, b)) @@ -150,8 +387,10 @@ class DataExpressionReducerTest { val b = EQuantityScale(ops.pure(1000.0), UnitFixture.m) val quantity = EQuantityAdd(a, b) val reducer = DataExpressionReducer( + Register.empty(), Register.empty(), ops, + sourceOps, ) // when/then @@ -167,7 +406,7 @@ class DataExpressionReducerTest { val a = UnitFixture.g val b = UnitFixture.kg val expected = EQuantityScale(ops.pure(1.001), EUnitLiteral(UnitSymbol.of("kg"), 1.0, DimensionFixture.mass)) - val reducer = DataExpressionReducer(Register.empty(), ops) + val reducer = DataExpressionReducer(Register.empty(), Register.empty(), ops, sourceOps) // when val actual = reducer.reduce(EQuantityAdd(a, b)) @@ -182,7 +421,7 @@ class DataExpressionReducerTest { val a = QuantityFixture.twoKilograms val b = UnitFixture.kg val expected = EQuantityScale(ops.pure(3.0), EUnitLiteral(UnitSymbol.of("kg"), 1.0, DimensionFixture.mass)) - val reducer = DataExpressionReducer(Register.empty(), ops) + val reducer = DataExpressionReducer(Register.empty(), Register.empty(), ops, sourceOps) // when val actual = reducer.reduce(EQuantityAdd(a, b)) @@ -197,7 +436,7 @@ class DataExpressionReducerTest { val a = UnitFixture.kg val b = QuantityFixture.twoKilograms val expected = EQuantityScale(ops.pure(3.0), EUnitLiteral(UnitSymbol.of("kg"), 1.0, DimensionFixture.mass)) - val reducer = DataExpressionReducer(Register.empty(), ops) + val reducer = DataExpressionReducer(Register.empty(), Register.empty(), ops, sourceOps) // when val actual = reducer.reduce(EQuantityAdd(a, b)) @@ -212,8 +451,10 @@ class DataExpressionReducerTest { val a = EQuantityScale(ops.pure(2.0), UnitFixture.kg) val b = EQuantityScale(ops.pure(1000.0), UnitFixture.g) val reducer = DataExpressionReducer( + Register.empty(), Register.empty(), ops, + sourceOps, ) // when @@ -230,8 +471,10 @@ class DataExpressionReducerTest { val a = EQuantityScale(ops.pure(2.0), UnitFixture.kg) val b = EQuantityScale(ops.pure(1000.0), UnitFixture.m) val reducer = DataExpressionReducer( + Register.empty(), Register.empty(), ops, + sourceOps, ) val eQuantitySub = EQuantitySub(a, b) @@ -248,7 +491,7 @@ class DataExpressionReducerTest { val a = UnitFixture.kg val b = UnitFixture.kg val expected = EQuantityScale(ops.pure(0.0), UnitFixture.kg) - val reducer = DataExpressionReducer(Register.empty(), ops) + val reducer = DataExpressionReducer(Register.empty(), Register.empty(), ops, sourceOps) // when val actual = reducer.reduce(EQuantitySub(a, b)) @@ -263,7 +506,7 @@ class DataExpressionReducerTest { val a = UnitFixture.kg val b = UnitFixture.g val expected = EQuantityScale(ops.pure(0.999), EUnitLiteral(UnitSymbol.of("kg"), 1.0, DimensionFixture.mass)) - val reducer = DataExpressionReducer(Register.empty(), ops) + val reducer = DataExpressionReducer(Register.empty(), Register.empty(), ops, sourceOps) // when val actual = reducer.reduce(EQuantitySub(a, b)) @@ -278,7 +521,7 @@ class DataExpressionReducerTest { val a = QuantityFixture.twoKilograms val b = UnitFixture.kg val expected = EQuantityScale(ops.pure(1.0), EUnitLiteral(UnitSymbol.of("kg"), 1.0, DimensionFixture.mass)) - val reducer = DataExpressionReducer(Register.empty(), ops) + val reducer = DataExpressionReducer(Register.empty(), Register.empty(), ops, sourceOps) // when val actual = reducer.reduce(EQuantitySub(a, b)) @@ -293,7 +536,7 @@ class DataExpressionReducerTest { val a = UnitFixture.kg val b = EQuantityScale(ops.pure(0.5), UnitFixture.kg) val expected = EQuantityScale(ops.pure(0.5), EUnitLiteral(UnitSymbol.of("kg"), 1.0, DimensionFixture.mass)) - val reducer = DataExpressionReducer(Register.empty(), ops) + val reducer = DataExpressionReducer(Register.empty(), Register.empty(), ops, sourceOps) // when val actual = reducer.reduce(EQuantitySub(a, b)) @@ -308,8 +551,10 @@ class DataExpressionReducerTest { val a = EQuantityScale(ops.pure(2.0), UnitFixture.person) val b = EQuantityScale(ops.pure(2.0), UnitFixture.km) val reducer = DataExpressionReducer( + Register.empty(), Register.empty(), ops, + sourceOps, ) // when @@ -333,8 +578,10 @@ class DataExpressionReducerTest { val a = UnitFixture.person val b = UnitFixture.km val reducer = DataExpressionReducer( + Register.empty(), Register.empty(), ops, + sourceOps, ) // when @@ -365,8 +612,10 @@ class DataExpressionReducerTest { // when val reducer = DataExpressionReducer( + Register.empty(), Register.empty(), ops, + sourceOps, ) // when @@ -389,8 +638,10 @@ class DataExpressionReducerTest { // when val reducer = DataExpressionReducer( + Register.empty(), Register.empty(), ops, + sourceOps, ) // when @@ -406,8 +657,10 @@ class DataExpressionReducerTest { val a = EQuantityScale(ops.pure(4.0), UnitFixture.km) val b = EQuantityScale(ops.pure(2.0), UnitFixture.hour) val reducer = DataExpressionReducer( + Register.empty(), Register.empty(), ops, + sourceOps, ) // when @@ -430,7 +683,7 @@ class DataExpressionReducerTest { // given val a = UnitFixture.km val b = UnitFixture.hour - val reducer = DataExpressionReducer(Register.empty(), ops) + val reducer = DataExpressionReducer(Register.empty(), Register.empty(), ops, sourceOps) // when val actual = reducer.reduce(EQuantityDiv(a, b)) @@ -461,7 +714,7 @@ class DataExpressionReducerTest { ) ) - val reducer = DataExpressionReducer(Register.empty(), ops) + val reducer = DataExpressionReducer(Register.empty(), Register.empty(), ops, sourceOps) // when val actual = reducer.reduce(EQuantityDiv(a, b)) @@ -476,8 +729,10 @@ class DataExpressionReducerTest { // given val a = EQuantityScale(ops.pure(4.0), UnitFixture.km) val reducer = DataExpressionReducer( + Register.empty(), Register.empty(), ops, + sourceOps, ) // when @@ -505,7 +760,9 @@ class DataExpressionReducerTest { DataKey("a") to EQuantityScale(ops.pure(1.0), UnitFixture.kg) ) ), + Register.empty(), ops, + sourceOps, ) // when @@ -527,8 +784,10 @@ class DataExpressionReducerTest { val quantityConversion = EQuantityScale(ops.pure(2.2), kg) val unitComposition = EUnitAlias("lbs", quantityConversion) val reducer = DataExpressionReducer( + Register.empty(), Register.empty(), ops, + sourceOps, ) // when val actual = reducer.reduce(unitComposition) @@ -547,8 +806,10 @@ class DataExpressionReducerTest { val quantityConversion = EQuantityScale(ops.pure(2200.0), g) val unitComposition = EUnitAlias("lbs", quantityConversion) val reducer = DataExpressionReducer( + Register.empty(), Register.empty(), ops, + sourceOps, ) // when val actual = reducer.reduce(unitComposition) @@ -564,7 +825,7 @@ class DataExpressionReducerTest { fun reduce_whenUnitCompositionComposition_shouldDeepReduce() { // given val expr = EUnitAlias("foo", EUnitAlias("bar", UnitFixture.kg)) - val reducer = DataExpressionReducer(Register.empty(), ops) + val reducer = DataExpressionReducer(Register.empty(), Register.empty(), ops, sourceOps) // when val actual = reducer.reduce(expr) @@ -587,7 +848,9 @@ class DataExpressionReducerTest { DataRegister( mapOf(DataKey("a") to UnitFixture.l) ), + Register.empty(), ops, + sourceOps, ) // when @@ -603,8 +866,10 @@ class DataExpressionReducerTest { // given val kg = UnitFixture.kg val reducer = DataExpressionReducer( + Register.empty(), Register.empty(), ops, + sourceOps, ) // when @@ -620,7 +885,7 @@ class DataExpressionReducerTest { // given val expr = EUnitOf(UnitFixture.l) val expected = QuantityFixture.oneLitre - val reducer = DataExpressionReducer(Register.empty(), ops) + val reducer = DataExpressionReducer(Register.empty(), Register.empty(), ops, sourceOps) // when val actual = reducer.reduce(expr) @@ -633,7 +898,7 @@ class DataExpressionReducerTest { fun reduce_whenUnitOfRef_shouldReturnAsIs() { // given val expr = EUnitOf(EDataRef("beer")) - val reducer = DataExpressionReducer(Register.empty(), ops) + val reducer = DataExpressionReducer(Register.empty(), Register.empty(), ops, sourceOps) // when val actual = reducer.reduce(expr) @@ -655,7 +920,7 @@ class DataExpressionReducerTest { DimensionFixture.mass.multiply(DimensionFixture.volume) ) ) - val reducer = DataExpressionReducer(Register.empty(), ops) + val reducer = DataExpressionReducer(Register.empty(), Register.empty(), ops, sourceOps) // when val actual = reducer.reduce(expr) @@ -677,7 +942,7 @@ class DataExpressionReducerTest { DimensionFixture.mass.multiply(DimensionFixture.volume) ) ) - val reducer = DataExpressionReducer(Register.empty(), ops) + val reducer = DataExpressionReducer(Register.empty(), Register.empty(), ops, sourceOps) // when val actual = reducer.reduce(expr) @@ -690,7 +955,7 @@ class DataExpressionReducerTest { fun reduce_whenUnitOfUnitOfRef_shouldMerge() { // given val expr = EUnitOf(EUnitOf(EDataRef("beer"))) - val reducer = DataExpressionReducer(Register.empty(), ops) + val reducer = DataExpressionReducer(Register.empty(), Register.empty(), ops, sourceOps) // when val actual = reducer.reduce(expr) @@ -706,8 +971,10 @@ class DataExpressionReducerTest { val kg = UnitFixture.kg val l = UnitFixture.l val reducer = DataExpressionReducer( + Register.empty(), Register.empty(), ops, + sourceOps, ) // when @@ -731,8 +998,10 @@ class DataExpressionReducerTest { val kg = UnitFixture.kg val l = UnitFixture.l val reducer = DataExpressionReducer( + Register.empty(), Register.empty(), ops, + sourceOps, ) // when @@ -755,8 +1024,10 @@ class DataExpressionReducerTest { // given val m = UnitFixture.m val reducer = DataExpressionReducer( + Register.empty(), Register.empty(), ops, + sourceOps, ) // when @@ -785,7 +1056,9 @@ class DataExpressionReducerTest { ) val reducer = DataExpressionReducer( units, + Register.empty(), ops, + sourceOps, ) // when @@ -807,7 +1080,9 @@ class DataExpressionReducerTest { val expression = EStringLiteral("FR") val reducer = DataExpressionReducer( DataRegister.empty(), + Register.empty(), ops, + sourceOps, ) // when @@ -828,7 +1103,9 @@ class DataExpressionReducerTest { DataKey("geo2") to EStringLiteral("FR"), ) ), + Register.empty(), ops, + sourceOps, ) // when @@ -850,7 +1127,9 @@ class DataExpressionReducerTest { DataKey("geo2") to EStringLiteral("FR"), ) ), + Register.empty(), ops, + sourceOps, ) // when diff --git a/core/src/test/kotlin/ch/kleis/lcaac/core/lang/evaluator/reducer/LcaExpressionReducerTest.kt b/core/src/test/kotlin/ch/kleis/lcaac/core/lang/evaluator/reducer/LcaExpressionReducerTest.kt index 0107e914..8d777d5c 100644 --- a/core/src/test/kotlin/ch/kleis/lcaac/core/lang/evaluator/reducer/LcaExpressionReducerTest.kt +++ b/core/src/test/kotlin/ch/kleis/lcaac/core/lang/evaluator/reducer/LcaExpressionReducerTest.kt @@ -1,16 +1,292 @@ package ch.kleis.lcaac.core.lang.evaluator.reducer -import ch.kleis.lcaac.core.lang.register.DataKey -import ch.kleis.lcaac.core.lang.register.DataRegister +import ch.kleis.lcaac.core.datasource.DataSourceOperations import ch.kleis.lcaac.core.lang.expression.* import ch.kleis.lcaac.core.lang.fixture.* +import ch.kleis.lcaac.core.lang.register.* 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 org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows import kotlin.test.assertEquals class LcaExpressionReducerTest { private val ops = BasicOperations + private val sourceOps = mockk>() + + /* + Block + */ + + @Test + fun reduce_whenBlockForEach_withLocalVariables() { + // given + val block = ETechnoBlockForEach( + "row", + "source", + mapOf("x" to ERecordEntry(EDataRef("row"), "mass")), + listOf( + ETechnoBlockEntry( + ETechnoExchange( + EDataRef("x"), + ProductFixture.carrot, + ) + ), + ) + ) + val expression = EProcess( + name = "foo", + inputs = listOf(block), + ) + val sourceOps = mockk>() + every { sourceOps.readAll(any()) } returns sequenceOf( + ERecord(mapOf( + "mass" to QuantityFixture.oneKilogram, + )), + ERecord(mapOf( + "mass" to QuantityFixture.twoKilograms, + )), + ) + val reducer = LcaExpressionReducer( + DataRegister.empty(), + DataSourceRegister.from(mapOf( + DataSourceKey("source") to mockk(), + )), + ops, + sourceOps, + ) + + // when + val actual = reducer.reduce(expression) + + // then + val expected = EProcess( + name = "foo", + inputs = listOf( + ETechnoBlockEntry( + ETechnoExchange( + QuantityFixture.oneKilogram, + ProductFixture.carrot, + ) + ), + ETechnoBlockEntry( + ETechnoExchange( + QuantityFixture.twoKilograms, + ProductFixture.carrot, + ) + ), + ), + ) + assertEquals(expected, actual) + } + + @Test + fun reduce_whenBlockForEach_withRecordEntryOverride_shouldThrow() { + // given + val block = ETechnoBlockForEach( + "row", + "source", + emptyMap(), + listOf( + ETechnoBlockEntry( + ETechnoExchange( + ERecordEntry(EDataRef("row"), "mass"), + ProductFixture.carrot, + ) + ), + ) + ) + val expression = EProcess( + name = "foo", + inputs = listOf(block), + ) + val sourceOps = mockk>() + every { sourceOps.readAll(any()) } returns sequenceOf( + ERecord(mapOf( + "mass" to QuantityFixture.oneKilogram, + )), + ERecord(mapOf( + "mass" to QuantityFixture.twoKilograms, + )), + ) + val reducer = LcaExpressionReducer( + DataRegister.from(mapOf( + DataKey("row") to QuantityFixture.oneLitre, + )), + DataSourceRegister.from(mapOf( + DataSourceKey("source") to mockk(), + )), + ops, + sourceOps, + ) + + // when/then + val e = assertThrows { reducer.reduce(expression) } + assertEquals("[row] is already bound", e.message) + } + + @Test + fun reduce_whenBlockForEach_withRecordEntry() { + // given + val block = ETechnoBlockForEach( + "row", + "source", + emptyMap(), + listOf( + ETechnoBlockEntry( + ETechnoExchange( + ERecordEntry(EDataRef("row"), "mass"), + ProductFixture.carrot, + ) + ), + ) + ) + val expression = EProcess( + name = "foo", + inputs = listOf(block), + ) + val sourceOps = mockk>() + every { sourceOps.readAll(any()) } returns sequenceOf( + ERecord(mapOf( + "mass" to QuantityFixture.oneKilogram, + )), + ERecord(mapOf( + "mass" to QuantityFixture.twoKilograms, + )), + ) + val reducer = LcaExpressionReducer( + DataRegister.empty(), + DataSourceRegister.from(mapOf( + DataSourceKey("source") to mockk(), + )), + ops, + sourceOps, + ) + + // when + val actual = reducer.reduce(expression) + + // then + val expected = EProcess( + name = "foo", + inputs = listOf( + ETechnoBlockEntry( + ETechnoExchange( + QuantityFixture.oneKilogram, + ProductFixture.carrot, + ) + ), + ETechnoBlockEntry( + ETechnoExchange( + QuantityFixture.twoKilograms, + ProductFixture.carrot, + ) + ), + ), + ) + assertEquals(expected, actual) + } + + @Test + fun reduce_whenBlockForEach_shouldFlatten() { + // given + val block = ETechnoBlockForEach( + "row", + "source", + emptyMap(), + listOf( + ETechnoBlockEntry( + ETechnoExchange( + QuantityFixture.oneKilogram, + ProductFixture.carrot, + ) + ), + EBlockForEach( + "row2", + "source", + emptyMap(), + listOf( + ETechnoBlockEntry( + ETechnoExchange( + QuantityFixture.oneLitre, + ProductFixture.water, + ) + ), + ) + ) + ) + ) + val expression = EProcess( + name = "foo", + inputs = listOf(block), + ) + val sourceOps = mockk>() + every { sourceOps.readAll(any()) } returns sequenceOf( + ERecord(emptyMap()), + ERecord(emptyMap()), + ) + val reducer = LcaExpressionReducer( + DataRegister.empty(), + DataSourceRegister.from(mapOf( + DataSourceKey("source") to mockk(), + )), + ops, + sourceOps, + ) + + // when + val actual = reducer.reduce(expression) + + // then + val expected = EProcess( + name = "foo", + inputs = listOf( + ETechnoBlockEntry( + ETechnoExchange( + QuantityFixture.oneKilogram, + ProductFixture.carrot, + ) + ), + ETechnoBlockEntry( + ETechnoExchange( + QuantityFixture.oneLitre, + ProductFixture.water, + ) + ), + ETechnoBlockEntry( + ETechnoExchange( + QuantityFixture.oneLitre, + ProductFixture.water, + ) + ), + ETechnoBlockEntry( + ETechnoExchange( + QuantityFixture.oneKilogram, + ProductFixture.carrot, + ) + ), + ETechnoBlockEntry( + ETechnoExchange( + QuantityFixture.oneLitre, + ProductFixture.water, + ) + ), + ETechnoBlockEntry( + ETechnoExchange( + QuantityFixture.oneLitre, + ProductFixture.water, + ) + ), + ), + ) + assertEquals(expected, actual) + } + + /* + Techno Exchange + */ @Test fun reduce_whenTechnoExchange_shouldReduceLabelSelectors() { @@ -30,7 +306,9 @@ class LcaExpressionReducerTest { DataRegister( mapOf(DataKey("geo") to EStringLiteral("FR")) ), + Register.empty(), ops, + sourceOps, ) // when @@ -60,10 +338,10 @@ class LcaExpressionReducerTest { ETechnoExchange(EDataRef("q_carrot"), ProductFixture.carrot), ), inputs = listOf( - ETechnoExchange(EDataRef("q_water"), ProductFixture.water) + ETechnoBlockEntry(ETechnoExchange(EDataRef("q_water"), ProductFixture.water)) ), biosphere = listOf( - EBioExchange(EDataRef("q_propanol"), SubstanceFixture.propanol), + EBioBlockEntry(EBioExchange(EDataRef("q_propanol"), SubstanceFixture.propanol)), ), ) val reducer = LcaExpressionReducer( @@ -74,7 +352,9 @@ class LcaExpressionReducerTest { "q_propanol" to QuantityFixture.oneKilogram, ).mapKeys { DataKey(it.key) } ), + Register.empty(), ops, + sourceOps, ) // when @@ -90,15 +370,19 @@ class LcaExpressionReducerTest { ), ), inputs = listOf( - ETechnoExchange( - QuantityFixture.oneLitre, - ProductFixture.water + ETechnoBlockEntry( + ETechnoExchange( + QuantityFixture.oneLitre, + ProductFixture.water + ) ) ), biosphere = listOf( - EBioExchange( - QuantityFixture.oneKilogram, - SubstanceFixture.propanol + EBioBlockEntry( + EBioExchange( + QuantityFixture.oneKilogram, + SubstanceFixture.propanol + ) ), ), ) @@ -118,7 +402,9 @@ class LcaExpressionReducerTest { DataKey("q") to QuantityFixture.oneKilogram, ) ), + Register.empty(), ops, + sourceOps, ) // when @@ -145,7 +431,9 @@ class LcaExpressionReducerTest { DataKey("q") to QuantityFixture.oneKilogram, ) ), + Register.empty(), ops, + sourceOps, ) // when @@ -172,7 +460,9 @@ class LcaExpressionReducerTest { DataKey("q") to QuantityFixture.oneKilogram, ) ), + Register.empty(), ops, + sourceOps, ) // when @@ -199,7 +489,9 @@ class LcaExpressionReducerTest { DataKey("kg") to UnitFixture.kg ) ), + Register.empty(), ops, + sourceOps, ) // when @@ -230,7 +522,9 @@ class LcaExpressionReducerTest { DataKey("kg") to UnitFixture.kg ) ), + Register.empty(), ops, + sourceOps, ) // when @@ -261,7 +555,9 @@ class LcaExpressionReducerTest { DataKey("kg") to UnitFixture.kg ) ), + Register.empty(), ops, + sourceOps, ) // when @@ -288,7 +584,9 @@ class LcaExpressionReducerTest { DataKey("kg") to UnitFixture.kg ) ), + Register.empty(), ops, + sourceOps, ) // when @@ -323,7 +621,9 @@ class LcaExpressionReducerTest { "kg" to UnitFixture.kg ).mapKeys { DataKey(it.key) } ), + Register.empty(), ops, + sourceOps, ) // when @@ -353,9 +653,11 @@ class LcaExpressionReducerTest { SubstanceFixture.propanol ), impacts = listOf( - EImpact( - EDataRef("q_cc"), - IndicatorFixture.climateChange + EImpactBlockEntry( + EImpact( + EDataRef("q_cc"), + IndicatorFixture.climateChange + ) ), ) ) @@ -366,7 +668,9 @@ class LcaExpressionReducerTest { "q_cc" to QuantityFixture.oneKilogram, ).mapKeys { DataKey(it.key) } ), + Register.empty(), ops, + sourceOps, ) // when @@ -379,9 +683,11 @@ class LcaExpressionReducerTest { SubstanceFixture.propanol ), impacts = listOf( - EImpact( - QuantityFixture.oneKilogram, - IndicatorFixture.climateChange + EImpactBlockEntry( + EImpact( + QuantityFixture.oneKilogram, + IndicatorFixture.climateChange + ) ), ) ) diff --git a/core/src/test/kotlin/ch/kleis/lcaac/core/lang/evaluator/reducer/ProcessTemplateExpressionReducerTest.kt b/core/src/test/kotlin/ch/kleis/lcaac/core/lang/evaluator/reducer/ProcessTemplateExpressionReducerTest.kt index d763dfec..85c8f11e 100644 --- a/core/src/test/kotlin/ch/kleis/lcaac/core/lang/evaluator/reducer/ProcessTemplateExpressionReducerTest.kt +++ b/core/src/test/kotlin/ch/kleis/lcaac/core/lang/evaluator/reducer/ProcessTemplateExpressionReducerTest.kt @@ -1,5 +1,6 @@ package ch.kleis.lcaac.core.lang.evaluator.reducer +import ch.kleis.lcaac.core.datasource.DataSourceOperations import ch.kleis.lcaac.core.lang.evaluator.EvaluatorException import ch.kleis.lcaac.core.lang.expression.* import ch.kleis.lcaac.core.lang.fixture.ProductFixture @@ -7,12 +8,14 @@ import ch.kleis.lcaac.core.lang.fixture.QuantityFixture import ch.kleis.lcaac.core.lang.fixture.UnitFixture import ch.kleis.lcaac.core.math.basic.BasicNumber import ch.kleis.lcaac.core.math.basic.BasicOperations +import io.mockk.mockk import org.junit.jupiter.api.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith class ProcessTemplateExpressionReducerTest { private val ops = BasicOperations + private val sourceOps = mockk>() @Test fun reduce_whenInstance_shouldReduce() { @@ -36,7 +39,7 @@ class ProcessTemplateExpressionReducerTest { ), ), inputs = listOf( - ETechnoExchange(EDataRef("q_water"), ProductFixture.water), + ETechnoBlockEntry(ETechnoExchange(EDataRef("q_water"), ProductFixture.water)), ), ) ) @@ -44,7 +47,7 @@ class ProcessTemplateExpressionReducerTest { Pair("q_carrot", QuantityFixture.twoKilograms), ) val expression = EProcessTemplateApplication(template, arguments) - val reducer = TemplateExpressionReducer(ops) + val reducer = TemplateExpressionReducer(ops, sourceOps) // when val actual = reducer.reduce(expression) @@ -70,9 +73,11 @@ class ProcessTemplateExpressionReducerTest { ), ), inputs = listOf( - ETechnoExchange( - QuantityFixture.oneLitre, - ProductFixture.water + ETechnoBlockEntry( + ETechnoExchange( + QuantityFixture.oneLitre, + ProductFixture.water + ) ), ), ) @@ -101,7 +106,7 @@ class ProcessTemplateExpressionReducerTest { ), ), inputs = listOf( - ETechnoExchange(EDataRef("q_water"), ProductFixture.water), + ETechnoBlockEntry(ETechnoExchange(EDataRef("q_water"), ProductFixture.water)), ), ) ) @@ -109,7 +114,7 @@ class ProcessTemplateExpressionReducerTest { Pair("foo", QuantityFixture.twoKilograms), ) val expression = EProcessTemplateApplication(template, arguments) - val reducer = TemplateExpressionReducer(ops) + val reducer = TemplateExpressionReducer(ops, sourceOps) // when/then val e = assertFailsWith(EvaluatorException::class, null) { reducer.reduce(expression) } diff --git a/core/src/test/kotlin/ch/kleis/lcaac/core/lang/evaluator/step/CompleteTerminalsTest.kt b/core/src/test/kotlin/ch/kleis/lcaac/core/lang/evaluator/step/CompleteTerminalsTest.kt index ce629b36..ef8bdcc8 100644 --- a/core/src/test/kotlin/ch/kleis/lcaac/core/lang/evaluator/step/CompleteTerminalsTest.kt +++ b/core/src/test/kotlin/ch/kleis/lcaac/core/lang/evaluator/step/CompleteTerminalsTest.kt @@ -22,7 +22,7 @@ class CompleteTerminalsTest { EProcess( name = "process", biosphere = listOf( - EBioExchange(QuantityFixture.oneKilogram, ESubstanceSpec("co2")) + EBioBlockEntry(EBioExchange(QuantityFixture.oneKilogram, ESubstanceSpec("co2"))) ), ) @@ -34,11 +34,13 @@ class CompleteTerminalsTest { EProcess( name = "process", biosphere = listOf( - EBioExchange( - QuantityFixture.oneKilogram, - ESubstanceSpec( - "co2", - referenceUnit = QuantityFixture.oneKilogram + EBioBlockEntry( + EBioExchange( + QuantityFixture.oneKilogram, + ESubstanceSpec( + "co2", + referenceUnit = QuantityFixture.oneKilogram + ) ) ) ), @@ -52,7 +54,7 @@ class CompleteTerminalsTest { val expression = ESubstanceCharacterization( EBioExchange(QuantityFixture.oneKilogram, SubstanceFixture.propanol), listOf( - EImpact(QuantityFixture.oneKilogram, EIndicatorSpec("cc")) + EImpactBlockEntry(EImpact(QuantityFixture.oneKilogram, EIndicatorSpec("cc"))) ) ) diff --git a/core/src/test/kotlin/ch/kleis/lcaac/core/lang/evaluator/step/ReduceLabelSelectorsTest.kt b/core/src/test/kotlin/ch/kleis/lcaac/core/lang/evaluator/step/ReduceLabelSelectorsTest.kt index e9cd9bf6..22a4123f 100644 --- a/core/src/test/kotlin/ch/kleis/lcaac/core/lang/evaluator/step/ReduceLabelSelectorsTest.kt +++ b/core/src/test/kotlin/ch/kleis/lcaac/core/lang/evaluator/step/ReduceLabelSelectorsTest.kt @@ -1,17 +1,21 @@ package ch.kleis.lcaac.core.lang.evaluator.step -import ch.kleis.lcaac.core.lang.register.DataKey -import ch.kleis.lcaac.core.lang.register.DataRegister +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.QuantityFixture +import ch.kleis.lcaac.core.lang.register.DataKey +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 io.mockk.mockk import org.junit.jupiter.api.Test import kotlin.test.assertEquals class ReduceLabelSelectorsTest { private val ops = BasicOperations + private val sourceOps = mockk>() @Test fun reduce_whenPassingByArguments() { @@ -22,14 +26,16 @@ class ReduceLabelSelectorsTest { body = EProcess( name = "salad_production", inputs = listOf( - ETechnoExchange( - quantity = QuantityFixture.oneKilogram, - product = EProductSpec( - name = "carrot", - referenceUnit = QuantityFixture.oneKilogram, - fromProcess = FromProcess( - name = "carrot_production", - matchLabels = MatchLabels(mapOf("geo" to EDataRef("geo"))), + ETechnoBlockEntry( + ETechnoExchange( + quantity = QuantityFixture.oneKilogram, + product = EProductSpec( + name = "carrot", + referenceUnit = QuantityFixture.oneKilogram, + fromProcess = FromProcess( + name = "carrot_production", + matchLabels = MatchLabels(mapOf("geo" to EDataRef("geo"))), + ) ) ) ) @@ -38,7 +44,7 @@ class ReduceLabelSelectorsTest { ), mapOf("geo" to EStringLiteral("FR")), ) - val reduceLabelSelectors = ReduceLabelSelectors(SymbolTable(), ops) + val reduceLabelSelectors = ReduceLabelSelectors(SymbolTable(), ops, sourceOps) // when val actual = reduceLabelSelectors.apply(instance) @@ -50,14 +56,16 @@ class ReduceLabelSelectorsTest { body = EProcess( "salad_production", inputs = listOf( - ETechnoExchange( - QuantityFixture.oneKilogram, - EProductSpec( - "carrot", + ETechnoBlockEntry( + ETechnoExchange( QuantityFixture.oneKilogram, - FromProcess( - name = "carrot_production", - matchLabels = MatchLabels(mapOf("geo" to EStringLiteral("FR"))), + EProductSpec( + "carrot", + QuantityFixture.oneKilogram, + FromProcess( + name = "carrot_production", + matchLabels = MatchLabels(mapOf("geo" to EStringLiteral("FR"))), + ) ) ) ) @@ -78,14 +86,16 @@ class ReduceLabelSelectorsTest { body = EProcess( name = "salad_production", inputs = listOf( - ETechnoExchange( - QuantityFixture.oneKilogram, - EProductSpec( - "carrot", + ETechnoBlockEntry( + ETechnoExchange( QuantityFixture.oneKilogram, - FromProcess( - name = "carrot_production", - matchLabels = MatchLabels(mapOf("geo" to EDataRef("geo"))), + EProductSpec( + "carrot", + QuantityFixture.oneKilogram, + FromProcess( + name = "carrot_production", + matchLabels = MatchLabels(mapOf("geo" to EDataRef("geo"))), + ) ) ) ) @@ -93,7 +103,7 @@ class ReduceLabelSelectorsTest { ) ), ) - val reduceLabelSelectors = ReduceLabelSelectors(SymbolTable(), ops) + val reduceLabelSelectors = ReduceLabelSelectors(SymbolTable(), ops, sourceOps) // when val actual = reduceLabelSelectors.apply(instance) @@ -105,14 +115,16 @@ class ReduceLabelSelectorsTest { body = EProcess( name = "salad_production", inputs = listOf( - ETechnoExchange( - QuantityFixture.oneKilogram, - EProductSpec( - "carrot", + ETechnoBlockEntry( + ETechnoExchange( QuantityFixture.oneKilogram, - FromProcess( - name = "carrot_production", - matchLabels = MatchLabels(mapOf("geo" to EStringLiteral("GLO"))), + EProductSpec( + "carrot", + QuantityFixture.oneKilogram, + FromProcess( + name = "carrot_production", + matchLabels = MatchLabels(mapOf("geo" to EStringLiteral("GLO"))), + ) ) ) ) @@ -132,14 +144,16 @@ class ReduceLabelSelectorsTest { body = EProcess( "salad_production", inputs = listOf( - ETechnoExchange( - QuantityFixture.oneKilogram, - EProductSpec( - "carrot", + ETechnoBlockEntry( + ETechnoExchange( QuantityFixture.oneKilogram, - FromProcess( - name = "carrot_production", - matchLabels = MatchLabels(mapOf("geo" to EDataRef("geo"))), + EProductSpec( + "carrot", + QuantityFixture.oneKilogram, + FromProcess( + name = "carrot_production", + matchLabels = MatchLabels(mapOf("geo" to EDataRef("geo"))), + ) ) ) ) @@ -147,7 +161,7 @@ class ReduceLabelSelectorsTest { ) ), ) - val reduceLabelSelectors = ReduceLabelSelectors(SymbolTable(), ops) + val reduceLabelSelectors = ReduceLabelSelectors(SymbolTable(), ops, sourceOps) // when val actual = reduceLabelSelectors.apply(instance) @@ -159,14 +173,16 @@ class ReduceLabelSelectorsTest { body = EProcess( name = "salad_production", inputs = listOf( - ETechnoExchange( - QuantityFixture.oneKilogram, - EProductSpec( - "carrot", + ETechnoBlockEntry( + ETechnoExchange( QuantityFixture.oneKilogram, - FromProcess( - name = "carrot_production", - matchLabels = MatchLabels(mapOf("geo" to EStringLiteral("GLO"))), + EProductSpec( + "carrot", + QuantityFixture.oneKilogram, + FromProcess( + name = "carrot_production", + matchLabels = MatchLabels(mapOf("geo" to EStringLiteral("GLO"))), + ) ) ) ) @@ -185,14 +201,16 @@ class ReduceLabelSelectorsTest { body = EProcess( name = "salad_production", inputs = listOf( - ETechnoExchange( - QuantityFixture.oneKilogram, - EProductSpec( - "carrot", + ETechnoBlockEntry( + ETechnoExchange( QuantityFixture.oneKilogram, - FromProcess( - name = "carrot_production", - matchLabels = MatchLabels(mapOf("geo" to EDataRef("geo"))), + EProductSpec( + "carrot", + QuantityFixture.oneKilogram, + FromProcess( + name = "carrot_production", + matchLabels = MatchLabels(mapOf("geo" to EDataRef("geo"))), + ) ) ) ) @@ -206,6 +224,7 @@ class ReduceLabelSelectorsTest { data = DataRegister(mapOf(DataKey("geo") to EStringLiteral("FR"))) ), ops, + sourceOps, ) // when @@ -217,14 +236,16 @@ class ReduceLabelSelectorsTest { body = EProcess( "salad_production", inputs = listOf( - ETechnoExchange( - QuantityFixture.oneKilogram, - EProductSpec( - "carrot", + ETechnoBlockEntry( + ETechnoExchange( QuantityFixture.oneKilogram, - FromProcess( - name = "carrot_production", - matchLabels = MatchLabels(mapOf("geo" to EStringLiteral("FR"))), + EProductSpec( + "carrot", + QuantityFixture.oneKilogram, + FromProcess( + name = "carrot_production", + matchLabels = MatchLabels(mapOf("geo" to EStringLiteral("FR"))), + ) ) ) ) @@ -244,14 +265,16 @@ class ReduceLabelSelectorsTest { "salad_production", labels = mapOf("geo" to EStringLiteral("FR")), inputs = listOf( - ETechnoExchange( - QuantityFixture.oneKilogram, - EProductSpec( - "carrot", + ETechnoBlockEntry( + ETechnoExchange( QuantityFixture.oneKilogram, - FromProcess( - name = "carrot_production", - matchLabels = MatchLabels(mapOf("geo" to EDataRef("geo"))), + EProductSpec( + "carrot", + QuantityFixture.oneKilogram, + FromProcess( + name = "carrot_production", + matchLabels = MatchLabels(mapOf("geo" to EDataRef("geo"))), + ) ) ) ) @@ -259,7 +282,7 @@ class ReduceLabelSelectorsTest { ) ), ) - val reduceLabelSelectors = ReduceLabelSelectors(SymbolTable(), ops) + val reduceLabelSelectors = ReduceLabelSelectors(SymbolTable(), ops, sourceOps) // when val actual = reduceLabelSelectors.apply(instance) @@ -271,14 +294,16 @@ class ReduceLabelSelectorsTest { "salad_production", labels = mapOf("geo" to EStringLiteral("FR")), inputs = listOf( - ETechnoExchange( - QuantityFixture.oneKilogram, - EProductSpec( - "carrot", + ETechnoBlockEntry( + ETechnoExchange( QuantityFixture.oneKilogram, - FromProcess( - name = "carrot_production", - matchLabels = MatchLabels(mapOf("geo" to EStringLiteral("FR"))), + EProductSpec( + "carrot", + QuantityFixture.oneKilogram, + FromProcess( + name = "carrot_production", + matchLabels = MatchLabels(mapOf("geo" to EStringLiteral("FR"))), + ) ) ) ) diff --git a/core/src/test/kotlin/ch/kleis/lcaac/core/lang/evaluator/step/ReduceTest.kt b/core/src/test/kotlin/ch/kleis/lcaac/core/lang/evaluator/step/ReduceTest.kt index 5c65fb7d..9eadd611 100644 --- a/core/src/test/kotlin/ch/kleis/lcaac/core/lang/evaluator/step/ReduceTest.kt +++ b/core/src/test/kotlin/ch/kleis/lcaac/core/lang/evaluator/step/ReduceTest.kt @@ -1,18 +1,23 @@ package ch.kleis.lcaac.core.lang.evaluator.step +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.evaluator.ToValue -import ch.kleis.lcaac.core.lang.expression.EProcessTemplateApplication -import ch.kleis.lcaac.core.lang.expression.EQuantityAdd +import ch.kleis.lcaac.core.lang.expression.* import ch.kleis.lcaac.core.lang.fixture.ProductValueFixture import ch.kleis.lcaac.core.lang.fixture.QuantityFixture import ch.kleis.lcaac.core.lang.fixture.QuantityValueFixture import ch.kleis.lcaac.core.lang.fixture.TemplateFixture +import ch.kleis.lcaac.core.lang.register.DataSourceKey +import ch.kleis.lcaac.core.lang.register.DataSourceRegister import ch.kleis.lcaac.core.lang.value.FromProcessRefValue import ch.kleis.lcaac.core.lang.value.ProcessValue import ch.kleis.lcaac.core.lang.value.TechnoExchangeValue +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 org.junit.jupiter.api.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -20,7 +25,59 @@ import kotlin.test.assertFailsWith class ReduceTest { private val ops = BasicOperations - + private val sourceOps = mockk>() + + @Test + fun eval_withDataSource() { + // given + val template = TemplateFixture.carrotProduction + val instance = EProcessTemplateApplication( + template, + mapOf( + "q_water" to ESumProduct("source", listOf("mass")) + ) + ) + val dataSource = ECsvSource( + location = "foo.csv", + schema = mapOf( + "mass" to ColumnType(QuantityFixture.oneLitre) + ) + ) + every { sourceOps.sumProduct(dataSource, listOf("mass")) } returns QuantityFixture.twoLitres + val symbolTable = SymbolTable( + dataSources = DataSourceRegister.from(mapOf( + DataSourceKey("source") to dataSource, + )) + ) + val reduceAndComplete = Reduce(symbolTable, ops, sourceOps) + + // when + val actual = with(ToValue(BasicOperations)) { reduceAndComplete.apply(instance).toValue() } + + // then + val expected = ProcessValue( + name = "carrot_production", + products = listOf( + TechnoExchangeValue( + QuantityValueFixture.oneKilogram, + ProductValueFixture.carrot.withFromProcessRef( + FromProcessRefValue( + name = "carrot_production", + arguments = mapOf("q_water" to QuantityValueFixture.twoLitres), + ) + ), + ) + ), + inputs = listOf( + TechnoExchangeValue( + QuantityValueFixture.twoLitres, + ProductValueFixture.water, + ) + ), + ) + assertEquals(expected, actual) + } + @Test fun eval_whenInstanceOfProcessTemplate_shouldEvaluateToProcessValue() { // given @@ -35,7 +92,7 @@ class ReduceTest { ) ) ) - val reduceAndComplete = Reduce(SymbolTable.empty(), ops) + val reduceAndComplete = Reduce(SymbolTable.empty(), ops, sourceOps) // when val actual = with(ToValue(BasicOperations)) { reduceAndComplete.apply(instance).toValue() } @@ -68,7 +125,7 @@ class ReduceTest { fun eval_whenProcessTemplate_shouldAutomaticallyInstantiateWithoutArguments() { // given val template = EProcessTemplateApplication(TemplateFixture.carrotProduction, emptyMap()) - val reduceAndComplete = Reduce(SymbolTable.empty(), ops) + val reduceAndComplete = Reduce(SymbolTable.empty(), ops, sourceOps) // when val actual = with(ToValue(BasicOperations)) { reduceAndComplete.apply(template).toValue() } @@ -101,7 +158,7 @@ class ReduceTest { fun eval_whenContainsUnboundedReference_shouldThrow() { // given val template = EProcessTemplateApplication(TemplateFixture.withUnboundedRef, emptyMap()) - val reduceAndComplete = Reduce(SymbolTable.empty(), ops) + val reduceAndComplete = Reduce(SymbolTable.empty(), ops, sourceOps) // when/then assertFailsWith( diff --git a/core/src/test/kotlin/ch/kleis/lcaac/core/lang/fixture/ImpactFixture.kt b/core/src/test/kotlin/ch/kleis/lcaac/core/lang/fixture/ImpactFixture.kt index 3261ef6b..44b1408d 100644 --- a/core/src/test/kotlin/ch/kleis/lcaac/core/lang/fixture/ImpactFixture.kt +++ b/core/src/test/kotlin/ch/kleis/lcaac/core/lang/fixture/ImpactFixture.kt @@ -1,6 +1,7 @@ package ch.kleis.lcaac.core.lang.fixture import ch.kleis.lcaac.core.lang.expression.EImpact +import ch.kleis.lcaac.core.lang.expression.EImpactBlockEntry import ch.kleis.lcaac.core.math.basic.BasicNumber object ImpactFixture { @@ -9,3 +10,9 @@ object ImpactFixture { indicator = IndicatorFixture.climateChange ) } + +object ImpactBlockFixture { + val oneClimateChange : EImpactBlockEntry = EImpactBlockEntry( + entry = ImpactFixture.oneClimateChange + ) +} diff --git a/core/src/test/kotlin/ch/kleis/lcaac/core/lang/fixture/ProcessFixture.kt b/core/src/test/kotlin/ch/kleis/lcaac/core/lang/fixture/ProcessFixture.kt index 277ea92a..ab1071f3 100644 --- a/core/src/test/kotlin/ch/kleis/lcaac/core/lang/fixture/ProcessFixture.kt +++ b/core/src/test/kotlin/ch/kleis/lcaac/core/lang/fixture/ProcessFixture.kt @@ -1,8 +1,6 @@ package ch.kleis.lcaac.core.lang.fixture -import ch.kleis.lcaac.core.lang.expression.EBioExchange -import ch.kleis.lcaac.core.lang.expression.EProcess -import ch.kleis.lcaac.core.lang.expression.ETechnoExchange +import ch.kleis.lcaac.core.lang.expression.* class ProcessFixture { companion object { @@ -13,13 +11,13 @@ class ProcessFixture { ETechnoExchange(QuantityFixture.oneKilogram, ProductFixture.carrot), ), inputs = listOf( - ETechnoExchange(QuantityFixture.oneLitre, ProductFixture.water), + ETechnoBlockEntry(ETechnoExchange(QuantityFixture.oneLitre, ProductFixture.water)), ), biosphere = listOf( - EBioExchange(QuantityFixture.oneKilogram, SubstanceFixture.propanol), + EBioBlockEntry(EBioExchange(QuantityFixture.oneKilogram, SubstanceFixture.propanol)), ), impacts = listOf( - ImpactFixture.oneClimateChange + EImpactBlockEntry(ImpactFixture.oneClimateChange), ) ) } diff --git a/core/src/test/kotlin/ch/kleis/lcaac/core/lang/fixture/SubstanceCharacterizationFixture.kt b/core/src/test/kotlin/ch/kleis/lcaac/core/lang/fixture/SubstanceCharacterizationFixture.kt index cb1dfcee..09fc08f6 100644 --- a/core/src/test/kotlin/ch/kleis/lcaac/core/lang/fixture/SubstanceCharacterizationFixture.kt +++ b/core/src/test/kotlin/ch/kleis/lcaac/core/lang/fixture/SubstanceCharacterizationFixture.kt @@ -1,9 +1,6 @@ package ch.kleis.lcaac.core.lang.fixture -import ch.kleis.lcaac.core.lang.expression.EBioExchange -import ch.kleis.lcaac.core.lang.expression.EImpact -import ch.kleis.lcaac.core.lang.expression.ESubstanceCharacterization -import ch.kleis.lcaac.core.lang.expression.ESubstanceSpec +import ch.kleis.lcaac.core.lang.expression.* import ch.kleis.lcaac.core.math.basic.BasicNumber class SubstanceCharacterizationFixture { @@ -11,7 +8,7 @@ class SubstanceCharacterizationFixture { val propanolCharacterization = ESubstanceCharacterization( referenceExchange = EBioExchange(QuantityFixture.oneKilogram, SubstanceFixture.propanol), impacts = listOf( - EImpact(QuantityFixture.oneKilogram, IndicatorFixture.climateChange), + EImpactBlockEntry(EImpact(QuantityFixture.oneKilogram, IndicatorFixture.climateChange)), ), ) @@ -19,7 +16,7 @@ class SubstanceCharacterizationFixture { ESubstanceCharacterization( referenceExchange = EBioExchange(QuantityFixture.oneKilogram, substance), impacts = listOf( - EImpact(QuantityFixture.oneKilogram, IndicatorFixture.climateChange) + EImpactBlockEntry(EImpact(QuantityFixture.oneKilogram, IndicatorFixture.climateChange)), ) ) } diff --git a/core/src/test/kotlin/ch/kleis/lcaac/core/lang/fixture/TemplateFixture.kt b/core/src/test/kotlin/ch/kleis/lcaac/core/lang/fixture/TemplateFixture.kt index ba784899..d258c75d 100644 --- a/core/src/test/kotlin/ch/kleis/lcaac/core/lang/fixture/TemplateFixture.kt +++ b/core/src/test/kotlin/ch/kleis/lcaac/core/lang/fixture/TemplateFixture.kt @@ -1,9 +1,6 @@ package ch.kleis.lcaac.core.lang.fixture -import ch.kleis.lcaac.core.lang.expression.EDataRef -import ch.kleis.lcaac.core.lang.expression.EProcess -import ch.kleis.lcaac.core.lang.expression.EProcessTemplate -import ch.kleis.lcaac.core.lang.expression.ETechnoExchange +import ch.kleis.lcaac.core.lang.expression.* class TemplateFixture { companion object { @@ -20,7 +17,7 @@ class TemplateFixture { ETechnoExchange(EDataRef("q_carrot"), ProductFixture.carrot), ), inputs = listOf( - ETechnoExchange(EDataRef("q_water"), ProductFixture.water), + ETechnoBlockEntry(ETechnoExchange(EDataRef("q_water"), ProductFixture.water)), ), ) ) @@ -34,7 +31,7 @@ class TemplateFixture { ETechnoExchange(QuantityFixture.twoKilograms, ProductFixture.carrot), ), inputs = listOf( - ETechnoExchange(QuantityFixture.oneGram, ProductFixture.carrot), + ETechnoBlockEntry(ETechnoExchange(QuantityFixture.oneGram, ProductFixture.carrot)), ), ) ) @@ -45,7 +42,7 @@ class TemplateFixture { ETechnoExchange(EDataRef("q_carrot"), ProductFixture.carrot), ), inputs = listOf( - ETechnoExchange(EDataRef("q_water"), ProductFixture.water), + ETechnoBlockEntry(ETechnoExchange(EDataRef("q_water"), ProductFixture.water)), ), ) ) 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/ProcessResolverTest.kt index b9abbfba..186d899a 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/ProcessResolverTest.kt @@ -22,7 +22,7 @@ class ProcessResolverTest { ETechnoExchange(EDataRef("q_carrot"), ProductFixture.carrot), ), inputs = listOf( - ETechnoExchange(EDataRef("q_water"), ProductFixture.water), + ETechnoBlockEntry(ETechnoExchange(EDataRef("q_water"), ProductFixture.water)), ), ) val carrotProductionFR = EProcessTemplate( @@ -75,7 +75,7 @@ class ProcessResolverTest { ETechnoExchange(EDataRef("q_carrot"), ProductFixture.carrot), ), inputs = listOf( - ETechnoExchange(EDataRef("q_water"), ProductFixture.water), + ETechnoBlockEntry(ETechnoExchange(EDataRef("q_water"), ProductFixture.water)), ), ) ) @@ -116,7 +116,7 @@ class ProcessResolverTest { ETechnoExchange(EDataRef("q_carrot"), ProductFixture.carrot), ), inputs = listOf( - ETechnoExchange(EDataRef("q_water"), ProductFixture.water), + ETechnoBlockEntry(ETechnoExchange(EDataRef("q_water"), ProductFixture.water)), ), ) ) @@ -127,7 +127,7 @@ class ProcessResolverTest { ETechnoExchange(QuantityFixture.oneKilogram, ProductFixture.salad), ), inputs = listOf( - ETechnoExchange(QuantityFixture.oneKilogram, ProductFixture.carrot), + ETechnoBlockEntry(ETechnoExchange(QuantityFixture.oneKilogram, ProductFixture.carrot)), ), ) ) @@ -168,7 +168,7 @@ class ProcessResolverTest { ETechnoExchange(EDataRef("q_carrot"), ProductFixture.carrot), ), inputs = listOf( - ETechnoExchange(EDataRef("q_water"), ProductFixture.water), + ETechnoBlockEntry(ETechnoExchange(EDataRef("q_water"), ProductFixture.water)), ), ) ) @@ -179,7 +179,7 @@ class ProcessResolverTest { ETechnoExchange(QuantityFixture.oneKilogram, ProductFixture.salad), ), inputs = listOf( - ETechnoExchange(QuantityFixture.oneKilogram, ProductFixture.carrot), + ETechnoBlockEntry(ETechnoExchange(QuantityFixture.oneKilogram, ProductFixture.carrot)), ), ) ) @@ -213,7 +213,7 @@ class ProcessResolverTest { ETechnoExchange(QuantityFixture.oneKilogram, ProductFixture.carrot), ), inputs = listOf( - ETechnoExchange(QuantityFixture.oneKilogram, ProductFixture.water), + ETechnoBlockEntry(ETechnoExchange(QuantityFixture.oneKilogram, ProductFixture.water)), ), ) ) @@ -225,7 +225,7 @@ class ProcessResolverTest { ETechnoExchange(QuantityFixture.oneKilogram, ProductFixture.carrot), ), inputs = listOf( - ETechnoExchange(QuantityFixture.oneKilogram, ProductFixture.water), + ETechnoBlockEntry(ETechnoExchange(QuantityFixture.oneKilogram, ProductFixture.water)), ), ) ) @@ -237,7 +237,7 @@ class ProcessResolverTest { ETechnoExchange(QuantityFixture.oneKilogram, ProductFixture.salad), ), inputs = listOf( - ETechnoExchange(QuantityFixture.oneKilogram, ProductFixture.carrot), + ETechnoBlockEntry(ETechnoExchange(QuantityFixture.oneKilogram, ProductFixture.carrot)), ), ) ) 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 a55c4868..b2752363 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 @@ -37,7 +37,7 @@ class BasicTestRunnerTest { arguments = emptyMap(), ) val symbolTable = SymbolTable.empty() - val runner = BasicTestRunner(symbolTable) + val runner = BasicTestRunner(symbolTable, mockk()) // when val actual = runner.run(case) @@ -77,7 +77,7 @@ class BasicTestRunnerTest { arguments = emptyMap(), ) val symbolTable = SymbolTable.empty() - val runner = BasicTestRunner(symbolTable) + val runner = BasicTestRunner(symbolTable, mockk()) // when val actual = runner.run(case) @@ -117,7 +117,7 @@ class BasicTestRunnerTest { arguments = emptyMap(), ) val symbolTable = SymbolTable.empty() - val runner = BasicTestRunner(symbolTable) + val runner = BasicTestRunner(symbolTable, mockk()) // when val actual = runner.run(case) @@ -141,7 +141,7 @@ class BasicTestRunnerTest { val evaluator = mockk>() every { evaluator.with(carrotProduction) } returns evaluator every { evaluator.trace(carrotProduction) } throws EvaluatorException("some error") - val runner = BasicTestRunner(SymbolTable.empty(), evaluator) + val runner = BasicTestRunner(SymbolTable.empty(), mockk(), evaluator) val case = TestCase( source = "source", name = "carrot_production", diff --git a/gradle.properties b/gradle.properties index 3723be23..d3d05b3d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,4 +2,4 @@ javaVersion=17 gradleVersion=7.6 org.gradle.jvmargs=-Xmx4096m lcaacGroup=ch.kleis.lcaac -lcaacVersion=1.3.4 +lcaacVersion=1.4 diff --git a/grammar/src/main/antlr/LcaLang.g4 b/grammar/src/main/antlr/LcaLang.g4 index 22292689..33f58062 100644 --- a/grammar/src/main/antlr/LcaLang.g4 +++ b/grammar/src/main/antlr/LcaLang.g4 @@ -1,7 +1,8 @@ grammar LcaLang; lcaFile - : pkg? pkgImport* ( processDefinition | testDefinition | unitDefinition | substanceDefinition | globalVariables )* EOF + : pkg? pkgImport* ( processDefinition | dataSourceDefinition | testDefinition | unitDefinition | + substanceDefinition | globalVariables )* EOF ; /* @@ -27,6 +28,29 @@ globalAssignment : dataRef EQUAL dataExpression ; +/* + Data source +*/ + +dataSourceDefinition + : DATASOURCE_KEYWORD dataSourceRef LBRACE + ( + locationField | schema + )* + RBRACE + ; +locationField + : LOCATION EQUAL STRING_LITERAL + ; +schema + : SCHEMA_KEYWORD LBRACE + columnDefinition* + RBRACE + ; +columnDefinition + : STRING_LITERAL EQUAL dataExpression + ; + /* Test */ @@ -138,7 +162,8 @@ variables ; assignment - : dataRef EQUAL dataExpression + : dataRef sep=EQUAL dataExpression + | dataRef sep=FROM_KEYWORD dataSourceRef ; /* @@ -174,16 +199,19 @@ block_impacts */ technoInputExchange - : quantity=dataExpression product=inputProductSpec + : quantity=dataExpression product=inputProductSpec # technoEntry + | FOR_EACH_KEYWORD dataRef IN_KEYWORD dataSourceRef LBRACE (variables | technoInputExchange)* RBRACE # technoBlockForEach ; technoProductExchange : quantity=dataExpression product=outputProductSpec ; bioExchange - : quantity=dataExpression substance=substanceSpec + : quantity=dataExpression substance=substanceSpec # bioEntry + | FOR_EACH_KEYWORD dataRef IN_KEYWORD dataSourceRef LBRACE (variables | bioExchange)* RBRACE # bioBlockForEach ; impactExchange - : quantity=dataExpression indicator=indicatorRef + : quantity=dataExpression indicator=indicatorRef # impactEntry + | FOR_EACH_KEYWORD dataRef IN_KEYWORD dataSourceRef LBRACE (variables | impactExchange)* RBRACE # impactBlockForEach ; @@ -198,7 +226,11 @@ dataExpression | left=dataExpression op=(PLUS | MINUS) right=dataExpression # addGroup | parenExpression # baseGroup | stringExpression # baseGroup - | dataRef # baseGroup + | dataRef slice? # baseGroup + | op=SUM LPAREN dataSourceRef COMMA columnRef (STAR columnRef)* RPAREN # colGroup + ; +slice + : LBRACK columnRef RBRACK ; parenExpression @@ -209,6 +241,10 @@ stringExpression : STRING_LITERAL ; +columnRef + : STRING_LITERAL + ; + /* Unit */ @@ -228,6 +264,7 @@ labelRef : uid ; dataRef : uid ; productRef : uid ; processRef : uid ; +dataSourceRef : uid ; substanceRef : uid ; indicatorRef : uid ; parameterRef : uid ; @@ -314,12 +351,22 @@ RESOURCES_KEYWORD : 'resources' ; MATCH_KEYWORD : 'match' ; LABELS_KEYWORD : 'labels' ; +DATASOURCE_KEYWORD : 'datasource' ; +LOCATION : 'location' ; +SCHEMA_KEYWORD : 'schema' ; + TEST_KEYWORD : 'test' ; GIVEN_KEYWORD : 'given' ; ASSERT_KEYWORD : 'assert' ; BETWEEN_KEYWORD : 'between' ; AND_KEYWORD : 'and' ; +FOR_EACH_KEYWORD : 'for_each' ; +IN_KEYWORD : 'in' ; + +SUM : 'sum' ; + + EQUAL : '=' ; LBRACK : '[' ; RBRACK : ']' ; 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 27af44b2..5c02eeb3 100644 --- a/grammar/src/main/kotlin/ch/kleis/lcaac/grammar/CoreMapper.kt +++ b/grammar/src/main/kotlin/ch/kleis/lcaac/grammar/CoreMapper.kt @@ -2,15 +2,13 @@ package ch.kleis.lcaac.grammar -import ch.kleis.lcaac.core.lang.* +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.register.* import ch.kleis.lcaac.core.math.QuantityOperations -import ch.kleis.lcaac.core.math.basic.BasicNumber -import ch.kleis.lcaac.core.testing.TestCase import ch.kleis.lcaac.grammar.parser.LcaLangParser import org.antlr.v4.runtime.tree.TerminalNode import java.lang.Double.parseDouble @@ -21,6 +19,7 @@ class CoreMapper( fun process( ctx: LcaLangParser.ProcessDefinitionContext, globals: DataRegister = DataRegister.empty(), + dataSources: Register>, ): EProcessTemplate { val name = ctx.name.innerText() val labels = ctx.labels() @@ -28,16 +27,17 @@ class CoreMapper( .associate { it.labelRef().innerText() to EStringLiteral(it.STRING_LITERAL().innerText()) } val locals = ctx.variables() .flatMap { it.assignment() } - .associate { it.dataRef().innerText() to dataExpression(it.dataExpression()) } + .associate { assignment(it) } val params = ctx.params() .flatMap { it.assignment() } - .associate { it.dataRef().innerText() to dataExpression(it.dataExpression()) } + .associate { assignment(it) } val symbolTable = SymbolTable( data = try { DataRegister(globals.plus(params.mapKeys { DataKey(it.key) }).plus(locals.mapKeys { DataKey(it.key) })) } catch (e: RegisterException) { throw EvaluatorException("Conflict between local variable(s) ${e.duplicates} and a global definition.") }, + dataSources = dataSources, ) val products = ctx.block_products() .flatMap { it.technoProductExchange() } @@ -73,11 +73,35 @@ class CoreMapper( ) } - fun impactExchange(ctx: LcaLangParser.ImpactExchangeContext): EImpact { - return EImpact( - dataExpression(ctx.quantity), - indicatorSpec(ctx.indicator), - ) + fun assignment(ctx: LcaLangParser.AssignmentContext): Pair> { + return when(ctx.sep.text) { + ctx.EQUAL()?.innerText() -> ctx.dataRef().innerText() to dataExpression(ctx.dataExpression()) + ctx.FROM_KEYWORD()?.innerText() -> ctx.dataRef().innerText() to EDefaultRecordOf(ctx.dataSourceRef().innerText()) + else -> throw IllegalStateException("parsing error: invalid assignment '${ctx.text}'") + } + } + + fun impactExchange(ctx: LcaLangParser.ImpactExchangeContext): ImpactBlock { + return when (ctx) { + is LcaLangParser.ImpactEntryContext -> EImpactBlockEntry( + EImpact( + dataExpression(ctx.quantity), + indicatorSpec(ctx.indicator), + ) + ) + + is LcaLangParser.ImpactBlockForEachContext -> { + val rowRef = ctx.dataRef().innerText() + val dataSourceRef = ctx.dataSourceRef().innerText() + val body = ctx.impactExchange().map { this.impactExchange(it) } + val locals = ctx.variables() + .flatMap { it.assignment() } + .associate { assignment(it) } + EImpactBlockForEach(rowRef, dataSourceRef, locals, body) + } + + else -> throw IllegalStateException("parsing error: expecting an impact exchange context") + } } fun indicatorSpec(ctx: LcaLangParser.IndicatorRefContext): EIndicatorSpec { @@ -90,12 +114,29 @@ class CoreMapper( ctx: LcaLangParser.BioExchangeContext, symbolTable: SymbolTable, type: SubstanceType - ): EBioExchange { - val quantity = dataExpression(ctx.quantity) - return EBioExchange( - quantity, - substanceSpec(ctx.substance, type, quantity, symbolTable) - ) + ): BioBlock { + return when (ctx) { + is LcaLangParser.BioEntryContext -> { + val quantity = dataExpression(ctx.quantity) + EBioBlockEntry( + EBioExchange( + quantity, + substanceSpec(ctx.substance, type, quantity, symbolTable), + ) + ) + } + + is LcaLangParser.BioBlockForEachContext -> { + val rowRef = ctx.dataRef().innerText() + val dataSourceRef = ctx.dataSourceRef().innerText() + val body = ctx.bioExchange().map { this.bioExchange(it, symbolTable, type) } + val locals = ctx.variables() + .flatMap { it.assignment() } + .associate { assignment(it) } + EBioBlockForEach(rowRef, dataSourceRef, locals, body) + } + else -> throw IllegalStateException("parsing error: expecting a bio exchange context") + } } fun substanceSpec( @@ -131,11 +172,27 @@ class CoreMapper( } - fun technoInputExchange(ctx: LcaLangParser.TechnoInputExchangeContext): ETechnoExchange { - return ETechnoExchange( - dataExpression(ctx.quantity), - inputProductSpec(ctx.product), - ) + fun technoInputExchange(ctx: LcaLangParser.TechnoInputExchangeContext): TechnoBlock { + return when (ctx) { + is LcaLangParser.TechnoEntryContext -> ETechnoBlockEntry( + ETechnoExchange( + dataExpression(ctx.quantity), + inputProductSpec(ctx.product), + ) + ) + + is LcaLangParser.TechnoBlockForEachContext -> { + val rowRef = ctx.dataRef().innerText() + val dataSourceRef = ctx.dataSourceRef().innerText() + val body = ctx.technoInputExchange().map { this.technoInputExchange(it) } + val locals = ctx.variables() + .flatMap { it.assignment() } + .associate { assignment(it) } + ETechnoBlockForEach(rowRef, dataSourceRef, locals, body) + } + + else -> throw IllegalStateException("parsing error: expecting a techno input exchange context") + } } fun inputProductSpec(ctx: LcaLangParser.InputProductSpecContext): EProductSpec { @@ -231,6 +288,18 @@ class CoreMapper( fun dataExpression(ctx: LcaLangParser.DataExpressionContext): DataExpression { return when (ctx) { + is LcaLangParser.ColGroupContext -> { + when (ctx.op.text) { + ctx.SUM().innerText() -> { + val sourceRef = ctx.dataSourceRef().innerText() + val columns = ctx.columnRef() + .map { it.innerText() } + ESumProduct(sourceRef, columns) + } + else -> throw IllegalStateException("parsing error: invalid column operation '${ctx.op.text}'") + } + } + is LcaLangParser.AddGroupContext -> { val left = dataExpression(ctx.left) val right = dataExpression(ctx.right) @@ -283,6 +352,10 @@ class CoreMapper( dataExpression(it.dataExpression()) } ?: ctx.stringExpression()?.let { EStringLiteral(it.STRING_LITERAL().innerText()) + } ?: ctx.slice()?.let { + val recordRef = ctx.dataRef().innerText() + val columnRef = ctx.slice().columnRef().innerText() + ERecordEntry(EDataRef(recordRef), columnRef) } ?: ctx.dataRef()?.let { EDataRef(it.innerText()) } ?: throw IllegalStateException() @@ -308,6 +381,14 @@ class CoreMapper( return this.uid().ID().innerText() } + fun LcaLangParser.ColumnRefContext.innerText(): String { + return this.STRING_LITERAL().innerText() + } + + fun LcaLangParser.DataSourceRefContext.innerText(): String { + return this.uid().ID().innerText() + } + fun LcaLangParser.ProcessRefContext.innerText(): String { return this.uid().ID().innerText() } @@ -339,4 +420,22 @@ class CoreMapper( val subCompartment = this.subCompartmentField()?.STRING_LITERAL()?.innerText() return SubstanceKey(name, type, compartment, subCompartment) } + + fun dataSourceDefinition(ctx: LcaLangParser.DataSourceDefinitionContext): DataSourceExpression { + val name = ctx.dataSourceRef().uid().ID().innerText() + val locationField = ctx.locationField().firstOrNull() + ?: throw LoaderException("missing location field in datasource $name") + val location = locationField.STRING_LITERAL().innerText() + val schemaBlock = ctx.schema().firstOrNull() + ?: throw LoaderException("missing schema in datasource $name") + val schema = schemaBlock.columnDefinition().associate { column -> + val key = column.STRING_LITERAL().innerText() + val value = dataExpression(column.dataExpression()) + key to ColumnType(value) + } + return ECsvSource( + location, + schema, + ) + } } diff --git a/grammar/src/main/kotlin/ch/kleis/lcaac/grammar/Loader.kt b/grammar/src/main/kotlin/ch/kleis/lcaac/grammar/Loader.kt index a02a5d6b..64bd36d8 100644 --- a/grammar/src/main/kotlin/ch/kleis/lcaac/grammar/Loader.kt +++ b/grammar/src/main/kotlin/ch/kleis/lcaac/grammar/Loader.kt @@ -23,6 +23,7 @@ class Loader( val unitDefinitions = files.flatMap { it.unitDefinition() } val processDefinitions = files.flatMap { it.processDefinition() } val substanceDefinitions = files.flatMap { it.substanceDefinition() } + val dataSourceDefinitions = files.flatMap { it.dataSourceDefinition() } val globalDefinitions = files.flatMap { it.globalVariables() } .flatMap { it.globalAssignment() } @@ -78,20 +79,32 @@ class Loader( throw LoaderException("Duplicate global variable ${e.duplicates} defined") } + val dataSources = try { + DataSourceRegister() + .plus( + dataSourceDefinitions + .map { DataSourceKey(it.dataSourceRef().innerText()) to dataSourceDefinition(it) } + .asIterable() + ) + } catch (e: RegisterException) { + throw LoaderException("Duplicate data sources ${e.duplicates} defined") + } val processTemplates = try { ProcessTemplateRegister() .plus( processDefinitions - .map { Pair(it.buildUniqueKey(), process(it, globals)) } + .map { Pair(it.buildUniqueKey(), process(it, globals, dataSources)) } .asIterable() ) } catch (e: RegisterException) { throw LoaderException("Duplicate process ${e.duplicates} defined") } + return SymbolTable( data = globals, processTemplates = processTemplates, + dataSources = dataSources, dimensions = dimensions, substanceCharacterizations = substanceCharacterizations, ) diff --git a/grammar/src/test/kotlin/ch/kleis/lcaac/grammar/CoreMapperTest.kt b/grammar/src/test/kotlin/ch/kleis/lcaac/grammar/CoreMapperTest.kt new file mode 100644 index 00000000..b266f788 --- /dev/null +++ b/grammar/src/test/kotlin/ch/kleis/lcaac/grammar/CoreMapperTest.kt @@ -0,0 +1,217 @@ +package ch.kleis.lcaac.grammar + +import ch.kleis.lcaac.core.lang.SymbolTable +import ch.kleis.lcaac.core.lang.expression.* +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 + +/* + TODO: Boyscout rule: Add mapper tests. + */ + +class CoreMapperTest { + private val ops = BasicOperations + + @Test + fun assignment_regular() { + // given + val ctx = LcaLangFixture.parser(""" + x = 1 kg + """.trimIndent()).assignment() + val mapper = CoreMapper(ops) + + // when + val actual = mapper.assignment(ctx) + + // then + val expected = "x" to EQuantityScale(BasicNumber(1.0), EDataRef("kg")) + assertEquals(expected, actual) + } + + @Test + fun assignment_defaultRecordOf() { + // given + val ctx = LcaLangFixture.parser(""" + x from inventory + """.trimIndent()).assignment() + val mapper = CoreMapper(ops) + + // when + val actual = mapper.assignment(ctx) + + // then + val expected = "x" to EDefaultRecordOf("inventory") + assertEquals(expected, actual) + } + + @Test + fun recordEntry() { + // given + val ctx = LcaLangFixture.parser(""" + row["mass"] + """.trimIndent()).dataExpression() + val mapper = CoreMapper(ops) + + // when + val actual = mapper.dataExpression(ctx) + + // then + val expected = ERecordEntry(EDataRef("row"), "mass") + assertEquals(expected, actual) + } + + @Test + fun columnOperation_sum() { + // given + val ctx = LcaLangFixture.parser(""" + sum(source, "mass" * "ratio") + """.trimIndent()).dataExpression() + val mapper = CoreMapper(ops) + + // when + val actual = mapper.dataExpression(ctx) + + // then + val expected = ESumProduct("source", listOf("mass", "ratio")) + assertEquals(expected, actual) + } + + @Test + fun datasource() { + // given + val ctx = LcaLangFixture.parser(""" + datasource source { + location = "file.csv" + schema { + "mass" = 1 kg + "geo" = "FR" + } + } + """.trimIndent()).dataSourceDefinition() + val mapper = CoreMapper(ops) + + // when + val actual = mapper.dataSourceDefinition(ctx) + + // then + val expected = ECsvSource( + location = "file.csv", + schema = mapOf( + "mass" to ColumnType(EQuantityScale(BasicNumber(1.0), EDataRef("kg"))), + "geo" to ColumnType(EStringLiteral("FR")), + ) + ) + assertEquals(expected, actual) + } + + @Test + fun technoInputExchange_blockForEach() { + // given + val ctx = LcaLangFixture.parser(""" + for_each row in source { + variables { + x = 1 l + } + 1 kg co2 + } + """.trimIndent()).technoInputExchange() + val mapper = CoreMapper(ops) + + // when + val actual = mapper.technoInputExchange(ctx) + + // then + val expected = ETechnoBlockForEach( + "row", + "source", + mapOf( + "x" to EQuantityScale(BasicNumber(1.0), EDataRef("l")) + ), + listOf( + ETechnoBlockEntry( + ETechnoExchange( + EQuantityScale(BasicNumber(1.0), EDataRef("kg")), + EProductSpec("co2"), + ) + ) + ) + ) + assertEquals(expected, actual) + } + + @Test + fun impactExchange_blockForEach() { + // given + val ctx = LcaLangFixture.parser(""" + for_each row in source { + variables { + x = 1 l + } + 1 kg co2 + } + """.trimIndent()).impactExchange() + val mapper = CoreMapper(ops) + + // when + val actual = mapper.impactExchange(ctx) + + // then + val expected = EImpactBlockForEach( + "row", + "source", + mapOf( + "x" to EQuantityScale(BasicNumber(1.0), EDataRef("l")) + ), + listOf( + EImpactBlockEntry( + EImpact( + EQuantityScale(BasicNumber(1.0), EDataRef("kg")), + EIndicatorSpec("co2"), + ) + ) + ) + ) + assertEquals(expected, actual) + } + + @Test + fun bioExchange_blockForEach() { + // given + val ctx = LcaLangFixture.parser(""" + for_each row in source { + variables { + x = 1 l + } + 1 kg co2(compartment="air") + } + """.trimIndent()).bioExchange() + val symbolTable = mockk>() + val substanceType = SubstanceType.EMISSION + val mapper = CoreMapper(ops) + + // when + val actual = mapper.bioExchange(ctx, symbolTable, substanceType) + + // then + val referenceUnit = EUnitOf(EQuantityClosure(symbolTable, EQuantityScale(BasicNumber(1.0), EDataRef("kg")))) + val expected = EBioBlockForEach( + "row", + "source", + mapOf( + "x" to EQuantityScale(BasicNumber(1.0), EDataRef("l")) + ), + listOf( + EBioBlockEntry( + EBioExchange( + EQuantityScale(BasicNumber(1.0), EDataRef("kg")), + ESubstanceSpec("co2", compartment = "air", type=substanceType, referenceUnit = referenceUnit), + ) + ) + ) + ) + assertEquals(expected, actual) + } +} diff --git a/grammar/src/test/kotlin/ch/kleis/lcaac/grammar/CoreTestMapperTest.kt b/grammar/src/test/kotlin/ch/kleis/lcaac/grammar/CoreTestMapperTest.kt index a0f10a1b..9277aa23 100644 --- a/grammar/src/test/kotlin/ch/kleis/lcaac/grammar/CoreTestMapperTest.kt +++ b/grammar/src/test/kotlin/ch/kleis/lcaac/grammar/CoreTestMapperTest.kt @@ -50,9 +50,11 @@ class CoreTestMapperTest { ) ), inputs = listOf( - ETechnoExchange( - EQuantityScale(BasicNumber(1.0), EDataRef("kWh")), - EProductSpec("electricity"), + ETechnoBlockEntry( + ETechnoExchange( + EQuantityScale(BasicNumber(1.0), EDataRef("kWh")), + EProductSpec("electricity"), + ) ) ) ) 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 d47bb700..0dac8f26 100644 --- a/grammar/src/test/kotlin/ch/kleis/lcaac/grammar/EvaluatorTest.kt +++ b/grammar/src/test/kotlin/ch/kleis/lcaac/grammar/EvaluatorTest.kt @@ -1,6 +1,7 @@ package ch.kleis.lcaac.grammar import ch.kleis.lcaac.core.assessment.ContributionAnalysisProgram +import ch.kleis.lcaac.core.datasource.DataSourceOperations import ch.kleis.lcaac.core.lang.dimension.Dimension import ch.kleis.lcaac.core.lang.dimension.UnitSymbol import ch.kleis.lcaac.core.lang.evaluator.Evaluator @@ -12,10 +13,58 @@ import ch.kleis.lcaac.core.lang.value.UnitValue import ch.kleis.lcaac.core.math.basic.BasicNumber import ch.kleis.lcaac.core.math.basic.BasicOperations import ch.kleis.lcaac.grammar.LcaLangFixture.Companion.lcaFile +import io.mockk.mockk import org.junit.jupiter.api.Test import kotlin.test.assertEquals class EvaluatorTest { + private val ops = BasicOperations + private val sourceOps = mockk>() + + @Test + fun arena_shouldHandleDefaultRecordOf() { + // given + val file = LcaLangFixture.parser(""" + datasource source { + location = "file.csv" + schema { + "mass" = 1 kg + } + } + + process p { + params { + row from source + } + products { + 1 kg carrot + } + impacts { + row["mass"] co2 + } + } + """.trimIndent()).lcaFile() + val loader = Loader(ops) + val symbolTable = loader.load(sequenceOf(file), listOf(LoaderOption.WITH_PRELUDE)) + val spec = EProductSpec( + name = "carrot", + fromProcess = FromProcess("p", MatchLabels(emptyMap())), + ) + val evaluator = Evaluator(symbolTable, ops, sourceOps) + + // when + val trace = evaluator.trace(setOf(spec)) + val program = ContributionAnalysisProgram(trace.getSystemValue(), trace.getEntryPoint()) + val analysis = program.run() + + // then + val port = analysis.getObservablePorts().get("carrot from p{}{row={mass=1.0 kg}}") + val indicator = analysis.getControllablePorts().get("co2") + val expected = QuantityValue(BasicNumber(1.0), UnitValue(UnitSymbol.of("kg"), 1.0, Dimension.of("mass"))) + val actual = analysis.getPortContribution(port, indicator) + assertEquals(expected, actual) + } + @Test fun arena_shouldHandleKnowledgeCorrectly() { // given @@ -53,13 +102,13 @@ class EvaluatorTest { } """.trimIndent() ) - val loader = Loader(BasicOperations) + val loader = Loader(ops) val symbolTable = loader.load(sequenceOf(file), listOf(LoaderOption.WITH_PRELUDE)) val spec = EProductSpec( name = "a", fromProcess = FromProcess("a_proc", MatchLabels(emptyMap())), ) - val evaluator = Evaluator(symbolTable, BasicOperations) + val evaluator = Evaluator(symbolTable, ops, sourceOps) // when val trace = evaluator.trace(setOf(spec)) @@ -101,13 +150,13 @@ class EvaluatorTest { } """.trimIndent() ) - val loader = Loader(BasicOperations) + val loader = Loader(ops) val symbolTable = loader.load(sequenceOf(file), listOf(LoaderOption.WITH_PRELUDE)) val spec = EProductSpec( name = "A", fromProcess = FromProcess("p1", MatchLabels(emptyMap())), ) - val evaluator = Evaluator(symbolTable, BasicOperations) + val evaluator = Evaluator(symbolTable, ops, sourceOps) // when val trace = evaluator.trace(setOf(spec)) @@ -139,13 +188,13 @@ class EvaluatorTest { } """.trimIndent() ) - val loader = Loader(BasicOperations) + val loader = Loader(ops) val symbolTable = loader.load(sequenceOf(file), listOf(LoaderOption.WITH_PRELUDE)) val spec = EProductSpec( name = "carrot", fromProcess = FromProcess("p", MatchLabels(emptyMap())), ) - val evaluator = Evaluator(symbolTable, BasicOperations) + val evaluator = Evaluator(symbolTable, ops, sourceOps) // when val trace = evaluator.trace(setOf(spec)) @@ -192,13 +241,13 @@ class EvaluatorTest { } """.trimIndent() ) - val loader = Loader(BasicOperations) + val loader = Loader(ops) val symbolTable = loader.load(sequenceOf(file), listOf(LoaderOption.WITH_PRELUDE)) val spec = EProductSpec( name = "carrot", fromProcess = FromProcess("p", MatchLabels(emptyMap())), ) - val evaluator = Evaluator(symbolTable, BasicOperations) + val evaluator = Evaluator(symbolTable, ops, sourceOps) // when val trace = evaluator.trace(setOf(spec)) @@ -236,13 +285,13 @@ class EvaluatorTest { } """.trimIndent() ) - val loader = Loader(BasicOperations) + val loader = Loader(ops) val symbolTable = loader.load(sequenceOf(file), listOf(LoaderOption.WITH_PRELUDE)) val spec = EProductSpec( name = "carrot", fromProcess = FromProcess("p", MatchLabels(emptyMap())), ) - val evaluator = Evaluator(symbolTable, BasicOperations) + val evaluator = Evaluator(symbolTable, ops, sourceOps) // when val trace = evaluator.trace(setOf(spec)) @@ -281,13 +330,13 @@ class EvaluatorTest { } """.trimIndent() ) - val loader = Loader(BasicOperations) + val loader = Loader(ops) val symbolTable = loader.load(sequenceOf(file), listOf(LoaderOption.WITH_PRELUDE)) val spec = EProductSpec( name = "carrot", fromProcess = FromProcess("p", MatchLabels(emptyMap())), ) - val evaluator = Evaluator(symbolTable, BasicOperations) + val evaluator = Evaluator(symbolTable, ops, sourceOps) // when val trace = evaluator.trace(setOf(spec)) @@ -336,13 +385,13 @@ class EvaluatorTest { } """.trimIndent() ) - val loader = Loader(BasicOperations) + val loader = Loader(ops) val symbolTable = loader.load(sequenceOf(file), listOf(LoaderOption.WITH_PRELUDE)) val spec = EProductSpec( name = "carrot", fromProcess = FromProcess("p", MatchLabels(emptyMap())), ) - val evaluator = Evaluator(symbolTable, BasicOperations) + val evaluator = Evaluator(symbolTable, ops, sourceOps) // when val trace = evaluator.trace(setOf(spec)) diff --git a/grammar/src/test/kotlin/ch/kleis/lcaac/grammar/LcaLangFixture.kt b/grammar/src/test/kotlin/ch/kleis/lcaac/grammar/LcaLangFixture.kt index bd8441fe..756567a2 100644 --- a/grammar/src/test/kotlin/ch/kleis/lcaac/grammar/LcaLangFixture.kt +++ b/grammar/src/test/kotlin/ch/kleis/lcaac/grammar/LcaLangFixture.kt @@ -7,6 +7,7 @@ import org.antlr.v4.runtime.CommonTokenStream class LcaLangFixture { companion object { + @Deprecated("use parser instead") fun lcaFile(content: String): LcaLangParser.LcaFileContext { val lexer = LcaLangLexer(CharStreams.fromString(content)) val tokens = CommonTokenStream(lexer) @@ -14,11 +15,18 @@ class LcaLangFixture { return parser.lcaFile() } + @Deprecated("use parser instead") fun test(content: String): LcaLangParser.TestDefinitionContext { val lexer = LcaLangLexer(CharStreams.fromString(content)) val tokens = CommonTokenStream(lexer) val parser = LcaLangParser(tokens) return parser.testDefinition() } + + fun parser(content: String): LcaLangParser { + val lexer = LcaLangLexer(CharStreams.fromString(content)) + val tokens = CommonTokenStream(lexer) + return LcaLangParser(tokens) + } } } 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 5bbc50fb..db1faf81 100644 --- a/grammar/src/test/kotlin/ch/kleis/lcaac/grammar/LoaderTest.kt +++ b/grammar/src/test/kotlin/ch/kleis/lcaac/grammar/LoaderTest.kt @@ -1,11 +1,11 @@ package ch.kleis.lcaac.grammar -import ch.kleis.lcaac.core.lang.register.DataKey -import ch.kleis.lcaac.core.lang.register.DataRegister 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.expression.* +import ch.kleis.lcaac.core.lang.register.DataKey +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.LcaLangFixture.Companion.lcaFile @@ -14,9 +14,110 @@ import kotlin.test.assertEquals import kotlin.test.assertNotNull class LoaderTest { + @Test + fun load_params_defaultRecord() { + // given + val file = LcaLangFixture.parser(""" + datasource source { + location = "file.csv" + schema { + "mass" = 1 kg + } + } + + process p { + params { + row from source + } + } + """.trimIndent()).lcaFile() + val loader = Loader(BasicOperations) + + // when + val actual = loader.load(sequenceOf(file)) + .getTemplate("p")!! + .params["row"]!! + + // then + val expected = EDefaultRecordOf("source") + assertEquals(expected, actual) + } + + @Test + fun load_whenSumOpInProduct_referenceUnitClosureShouldContainDataSource() { + // given + val file = LcaLangFixture.parser( + """ + datasource source { + location = "file.csv" + schema { + "mass" = 1 kg + } + } + + process p { + products { + sum(source["mass"]) p + } + } + """.trimIndent() + ).lcaFile() + val loader = Loader(BasicOperations) + + // when + val symbolTable = loader.load(sequenceOf(file)) + val referenceUnit = symbolTable + .getTemplate("p")!! + .body.products[0] + .product.referenceUnit!! as EUnitOf + val closure = referenceUnit.expression as EQuantityClosure + val actual = closure.symbolTable + + + // then + val expected = ECsvSource( + location = "file.csv", + schema = mapOf( + "mass" to ColumnType(EQuantityScale(BasicNumber(1.0), EDataRef("kg"))), + ) + ) + assertEquals(expected, actual.getDataSource("source")) + } + + @Test + fun load_datasource() { + // given + val file = LcaLangFixture.parser( + """ + datasource source { + location = "file.csv" + schema { + "mass" = 1 kg + "geo" = "FR" + } + } + """.trimIndent() + ).lcaFile() + val loader = Loader(BasicOperations) + + // when + val actual = loader.load(sequenceOf(file)) + .getDataSource("source")!! + + // then + val expected = ECsvSource( + location = "file.csv", + schema = mapOf( + "mass" to ColumnType(EQuantityScale(BasicNumber(1.0), EDataRef("kg"))), + "geo" to ColumnType(EStringLiteral("FR")), + ) + ) + assertEquals(expected, actual) + } + @Test fun load_whenFileContainsTest_thenNoError() { - val file = lcaFile( + val file = LcaLangFixture.parser( """ process p { products { @@ -36,7 +137,7 @@ class LoaderTest { } } """.trimIndent() - ) + ).lcaFile() val loader = Loader(BasicOperations) // when @@ -95,9 +196,10 @@ class LoaderTest { val loader = Loader(BasicOperations) // when - val actual = loader.load(sequenceOf(file)).getTemplate("p")!! + val input = loader.load(sequenceOf(file)).getTemplate("p")!! .body - .inputs[0] + .inputs[0] as ETechnoBlockEntry + val actual = input.entry .product.fromProcess!! // then @@ -293,18 +395,20 @@ class LoaderTest { val expected = ESubstanceCharacterization( referenceExchange = EBioExchange( EDataRef("kg"), ESubstanceSpec( - name = "co2", - displayName = "carbon dioxide", - type = SubstanceType.EMISSION, - compartment = compartment, - subCompartment = null, - referenceUnit = EUnitOf(EDataRef("kg")), - ) + name = "co2", + displayName = "carbon dioxide", + type = SubstanceType.EMISSION, + compartment = compartment, + subCompartment = null, + referenceUnit = EUnitOf(EDataRef("kg")), + ) ), impacts = listOf( - EImpact( - oneKg, - EIndicatorSpec("GWP", null) + EImpactBlockEntry( + EImpact( + oneKg, + EIndicatorSpec("GWP", null) + ) ) ), ) @@ -341,18 +445,20 @@ class LoaderTest { val expected = ESubstanceCharacterization( referenceExchange = EBioExchange( EDataRef("kg"), ESubstanceSpec( - name = "co2", - displayName = "carbon dioxide", - type = SubstanceType.EMISSION, - compartment = compartment, - subCompartment = subCompartment, - referenceUnit = EUnitOf(EDataRef("kg")), - ) + name = "co2", + displayName = "carbon dioxide", + type = SubstanceType.EMISSION, + compartment = compartment, + subCompartment = subCompartment, + referenceUnit = EUnitOf(EDataRef("kg")), + ) ), impacts = listOf( - EImpact( - oneKg, - EIndicatorSpec("GWP", null) + EImpactBlockEntry( + EImpact( + oneKg, + EIndicatorSpec("GWP", null) + ) ) ), ) @@ -563,7 +669,7 @@ class LoaderTest { """ process p { inputs { - 1 kg in from q(a = 1 kg) match (geo = "FR") + 1 kg q_in from q(a = 1 kg) match (geo = "FR") } } """.trimIndent() @@ -579,17 +685,19 @@ class LoaderTest { body = EProcess( "p", inputs = listOf( - ETechnoExchange( - oneKg, - EProductSpec( - "in", - referenceUnit = null, - FromProcess( - "q", - matchLabels = MatchLabels(mapOf("geo" to EStringLiteral("FR"))), - arguments = mapOf("a" to oneKg), + ETechnoBlockEntry( + ETechnoExchange( + oneKg, + EProductSpec( + "q_in", + referenceUnit = null, + FromProcess( + "q", + matchLabels = MatchLabels(mapOf("geo" to EStringLiteral("FR"))), + arguments = mapOf("a" to oneKg), + ), ), - ), + ) ) ) ) @@ -622,15 +730,17 @@ class LoaderTest { body = EProcess( "p", biosphere = listOf( - EBioExchange( - oneKg, - ESubstanceSpec( - "co2", - "co2", - SubstanceType.EMISSION, - compartment = null, - subCompartment = null, - referenceUnit, + EBioBlockEntry( + EBioExchange( + oneKg, + ESubstanceSpec( + "co2", + "co2", + SubstanceType.EMISSION, + compartment = null, + subCompartment = null, + referenceUnit, + ) ) ) ) @@ -664,15 +774,17 @@ class LoaderTest { body = EProcess( "p", biosphere = listOf( - EBioExchange( - oneKg, - ESubstanceSpec( - "co2", - "co2", - SubstanceType.EMISSION, - "air", - "low pop", - referenceUnit, + EBioBlockEntry( + EBioExchange( + oneKg, + ESubstanceSpec( + "co2", + "co2", + SubstanceType.EMISSION, + "air", + "low pop", + referenceUnit, + ) ) ) ) @@ -706,15 +818,17 @@ class LoaderTest { body = EProcess( "p", biosphere = listOf( - EBioExchange( - oneKg, - ESubstanceSpec( - "co2", - "co2", - SubstanceType.RESOURCE, - compartment = null, - subCompartment = null, - referenceUnit, + EBioBlockEntry( + EBioExchange( + oneKg, + ESubstanceSpec( + "co2", + "co2", + SubstanceType.RESOURCE, + compartment = null, + subCompartment = null, + referenceUnit, + ) ) ) ) @@ -748,15 +862,17 @@ class LoaderTest { body = EProcess( "p", biosphere = listOf( - EBioExchange( - oneKg, - ESubstanceSpec( - "co2", - "co2", - SubstanceType.RESOURCE, - "air", - "low pop", - referenceUnit, + EBioBlockEntry( + EBioExchange( + oneKg, + ESubstanceSpec( + "co2", + "co2", + SubstanceType.RESOURCE, + "air", + "low pop", + referenceUnit, + ) ) ) ) @@ -790,15 +906,17 @@ class LoaderTest { body = EProcess( "p", biosphere = listOf( - EBioExchange( - oneKg, - ESubstanceSpec( - "co2", - "co2", - SubstanceType.LAND_USE, - compartment = null, - subCompartment = null, - referenceUnit, + EBioBlockEntry( + EBioExchange( + oneKg, + ESubstanceSpec( + "co2", + "co2", + SubstanceType.LAND_USE, + compartment = null, + subCompartment = null, + referenceUnit, + ) ) ) ) @@ -832,15 +950,17 @@ class LoaderTest { body = EProcess( "p", biosphere = listOf( - EBioExchange( - oneKg, - ESubstanceSpec( - "co2", - "co2", - SubstanceType.LAND_USE, - "air", - "low pop", - referenceUnit, + EBioBlockEntry( + EBioExchange( + oneKg, + ESubstanceSpec( + "co2", + "co2", + SubstanceType.LAND_USE, + "air", + "low pop", + referenceUnit, + ) ) ) ) @@ -872,10 +992,12 @@ class LoaderTest { body = EProcess( "p", impacts = listOf( - EImpact( - oneKg, - EIndicatorSpec( - "co2", + EImpactBlockEntry( + EImpact( + oneKg, + EIndicatorSpec( + "co2", + ) ) ) ) @@ -890,11 +1012,11 @@ class LoaderTest { val file = lcaFile( """ variables { - sum = x + y - mul = x * y - div = x / y - scale = 2 x - pow = x^2.0 + op_sum = x + y + op_mul = x * y + op_div = x / y + op_scale = 2 x + op_pow = x^2.0 } """.trimIndent() ) @@ -909,11 +1031,11 @@ class LoaderTest { val div = EQuantityDiv(EDataRef("x"), EDataRef("y")) val scale = EQuantityScale(BasicNumber(2.0), EDataRef("x")) val pow = EQuantityPow(EDataRef("x"), 2.0) - assertEquals(sum, actual.getData("sum")) - assertEquals(mul, actual.getData("mul")) - assertEquals(div, actual.getData("div")) - assertEquals(scale, actual.getData("scale")) - assertEquals(pow, actual.getData("pow")) + assertEquals(sum, actual.getData("op_sum")) + assertEquals(mul, actual.getData("op_mul")) + assertEquals(div, actual.getData("op_div")) + assertEquals(scale, actual.getData("op_scale")) + assertEquals(pow, actual.getData("op_pow")) } @Test diff --git a/tutorials/02-advanced/05-datasources/inventory.csv b/tutorials/02-advanced/05-datasources/inventory.csv new file mode 100644 index 00000000..577279ea --- /dev/null +++ b/tutorials/02-advanced/05-datasources/inventory.csv @@ -0,0 +1,4 @@ +id,quantity,ram_size,storage_size,amortization_period,power,ram_allocation,storage_allocation,GWP,WU +small,38,384,61.44,5,400,75,25,2855.502608725893,6.468493286421048 +medium,62,384,11.52,5,400,75,25,10469.589982157144,23.680314019609003 +large,3,768,76.8,5,400,75,25,13787.498425573982,31.193930932287838 diff --git a/tutorials/02-advanced/05-datasources/main.lca b/tutorials/02-advanced/05-datasources/main.lca new file mode 100644 index 00000000..eaf344cd --- /dev/null +++ b/tutorials/02-advanced/05-datasources/main.lca @@ -0,0 +1,125 @@ +/* + A datasource is assumed to provide a sequence of records. + For now, only the local CSV file source is supported. +*/ +datasource inventory { + location = "inventory.csv" + + /* + The schema is defined using default values. Note that the unit of + the default value will be the one chosen for the entire column. + */ + schema { + "quantity" = 1 p + "ram_size" = 16 GB + "storage_size" = 1 TB + "amortization_period" = 5 year + "power" = 400 W + "ram_allocation" = 75 percent + "storage_allocation" = 25 percent + + // embodied impact + "GWP" = 0 kg_CO2_Eq + "WU" = 0 m3 + } +} + +/* + Block 'for_each' +*/ + +process simple_pool { + products { + 1 p simple_pool + } + impacts { + // A block for_each allows to map over the records of the datasource + for_each row in inventory { + + // The variable row represents a record. + // The dimension of, e.g., row["quantity"] is defined by the schema. + row["quantity"] * row["GWP"] GWP + } + } +} + +test simple_pool { + given { + 1 p simple_pool + } + assert { + GWP between 700e3 kg_CO2_Eq and 800e3 kg_CO2_Eq + } +} + + +/* + Record as parameter +*/ + +process server { + // You can define a parameter as a row from inventory. + // The default value for this parameter is given by the schema. + params { + row from inventory + } + products { + 1 p server + } + impacts { + row["GWP"] GWP + } +} + +process pool_server { + products { + 1 p pool_server + } + inputs { + for_each row in inventory { + // record variable can be fed to the process invoked. + row["quantity"] server from server(row = row) + } + } +} + +test pool_server { + given { + 1 p pool_server + } + assert { + GWP between 700e3 kg_CO2_Eq and 800e3 kg_CO2_Eq + } +} + +/* + Column operations. +*/ + +process sum_prod { + products { + 1 p sum_prod + } + impacts { + /* + The 'sum' primitive allows compute the sum-product + of multiple columns. + + In this example, the columns "quantity" and "GWP" + are multiplied point-wise, and then summed. + + For now, only the point-wise product of columns is + supported. + */ + sum(inventory, "quantity" * "GWP") GWP + } +} + +test sum_prod { + given { + 1 p sum_prod + } + assert { + GWP between 700e3 kg_CO2_Eq and 800e3 kg_CO2_Eq + } +} diff --git a/tutorials/02-advanced/05-datasources/units.lca b/tutorials/02-advanced/05-datasources/units.lca new file mode 100644 index 00000000..90b5cbf8 --- /dev/null +++ b/tutorials/02-advanced/05-datasources/units.lca @@ -0,0 +1,9 @@ +unit GB { + symbol = "GB" + dimension = "memory" +} + +unit TB { + symbol = "TB" + alias_for = 1024 GB +} diff --git a/tutorials/run.sh b/tutorials/run.sh index 70fa14b9..4050872d 100755 --- a/tutorials/run.sh +++ b/tutorials/run.sh @@ -27,6 +27,7 @@ lcaac test -p $TUTORIALS_PATH/02-advanced/01-parametrized-process lcaac test -p $TUTORIALS_PATH/02-advanced/02-variables lcaac test -p $TUTORIALS_PATH/02-advanced/03-units lcaac test -p $TUTORIALS_PATH/02-advanced/04-labels +lcaac test -p $TUTORIALS_PATH/02-advanced/05-datasources # Check custom dimensions tutorial set -euo