Skip to content

Commit

Permalink
Merge pull request #16 from kleis-technology/feature/cli-trace
Browse files Browse the repository at this point in the history
feature/cli trace
  • Loading branch information
pevab authored Jan 29, 2024
2 parents ea21124 + 9bb9b78 commit 60f3a91
Show file tree
Hide file tree
Showing 10 changed files with 255 additions and 51 deletions.
10 changes: 10 additions & 0 deletions cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ cd $GIT_ROOT/cli/samples
lcaac assess "electricity_mix" --file params.csv
```

## Tracing

The `trace` command traces the execution of the assessment of a (single-product) target process.
For every intermediate product or substance, it reports their quantity and their contribution to the total impact.

```bash
cd $GIT_ROOT/cli/samples
lcaac trace "electricity_mix"
```

## Run tests

You can run all the tests with the following command.
Expand Down
2 changes: 2 additions & 0 deletions cli/src/main/kotlin/ch/kleis/lcaac/cli/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ package ch.kleis.lcaac.cli
import ch.kleis.lcaac.cli.cmd.AssessCommand
import ch.kleis.lcaac.cli.cmd.LcaacCommand
import ch.kleis.lcaac.cli.cmd.TestCommand
import ch.kleis.lcaac.cli.cmd.TraceCommand
import com.github.ajalt.clikt.core.subcommands

fun main(args: Array<String>) = LcaacCommand()
.subcommands(
AssessCommand(),
TestCommand(),
TraceCommand(),
)
.main(args)
187 changes: 187 additions & 0 deletions cli/src/main/kotlin/ch/kleis/lcaac/cli/cmd/TraceCommand.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package ch.kleis.lcaac.cli.cmd

import ch.kleis.lcaac.core.assessment.ContributionAnalysisProgram
import ch.kleis.lcaac.core.datasource.CsvSourceOperations
import ch.kleis.lcaac.core.lang.evaluator.Evaluator
import ch.kleis.lcaac.core.lang.evaluator.EvaluatorException
import ch.kleis.lcaac.core.lang.evaluator.reducer.DataExpressionReducer
import ch.kleis.lcaac.core.lang.value.FullyQualifiedSubstanceValue
import ch.kleis.lcaac.core.lang.value.IndicatorValue
import ch.kleis.lcaac.core.lang.value.PartiallyQualifiedSubstanceValue
import ch.kleis.lcaac.core.lang.value.ProductValue
import ch.kleis.lcaac.core.math.basic.BasicOperations
import ch.kleis.lcaac.core.math.basic.BasicOperations.toDouble
import ch.kleis.lcaac.core.prelude.Prelude.Companion.sanitize
import ch.kleis.lcaac.grammar.Loader
import ch.kleis.lcaac.grammar.LoaderOption
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.arguments.help
import com.github.ajalt.clikt.parameters.options.*
import com.github.ajalt.clikt.parameters.types.file
import org.apache.commons.csv.CSVFormat
import org.apache.commons.csv.CSVPrinter
import java.io.File

@Suppress("MemberVisibilityCanBePrivate")
class TraceCommand : CliktCommand(name = "trace", help = "Trace the contributions") {
val name: String by argument().help("Process name")
val labels: Map<String, String> by option("-l", "--label")
.help(
"""
Specify a process label as a key value pair.
Example: lcaac assess <process name> -l model="ABC" -l geo="FR".
""".trimIndent())
.associate()
private val getPath = option("-p", "--path").file(canBeFile = false).default(File(".")).help("Path to root folder.")
val path: File by getPath
val dataSourcePath: File by option("--data-path").file(canBeFile = false)
.defaultLazy { getPath.value }
.help("Path to data folder. Default to root folder.")
val arguments: Map<String, String> by option("-D", "--parameter")
.help(
"""
Override parameter value as a key value pair.
Example: `lcaac assess <process name> -D x="12 kg" -D geo="UK" -f params.csv`.
""".trimIndent())
.associate()

override fun run() {
val ops = BasicOperations
val sourceOps = CsvSourceOperations(dataSourcePath, ops)
val files = lcaFiles(path)
val symbolTable = Loader(ops).load(files, listOf(LoaderOption.WITH_PRELUDE))
val evaluator = Evaluator(symbolTable, ops, sourceOps)
val template = symbolTable.getTemplate(name, labels)
?: throw EvaluatorException("unknown template $name$labels")
val args = prepareArguments(
DataExpressionReducer(symbolTable.data, symbolTable.dataSources, ops, sourceOps),
template,
arguments,
)
val trace = evaluator.trace(template, args)
val system = trace.getSystemValue()
val entryPoint = trace.getEntryPoint()

val program = ContributionAnalysisProgram(system, entryPoint)
val analysis = program.run()

val observablePorts = analysis.getObservablePorts()
.getElements()
.sortedWith(trace.getComparator())
val controllablePorts = analysis.getControllablePorts().getElements()
.sortedBy { it.getShortName() }

val header = listOf(
"d_amount", "d_unit", "d_product", "alloc",
"name", "a", "b", "c", "amount", "unit",
).plus(
controllablePorts.flatMap {
listOf(
sanitize(it.getDisplayName()),
"${sanitize(it.getDisplayName())}_unit"
)
}
)

val products = entryPoint.products.asSequence()
val lines = products.flatMap { demandedProduct ->
val demandedAmount = demandedProduct.quantity.amount
val demandedUnit = demandedProduct.quantity.unit
val demandedProductName = demandedProduct.product.name
val allocationAmount = (demandedProduct.allocation?.amount?.toDouble() ?: 1.0) * (demandedProduct.allocation?.unit?.scale ?: 1.0)
observablePorts.asSequence()
.map { row ->
val supply = analysis.supplyOf(row)
val supplyAmount = supply.amount.value * allocationAmount
val prefix = when (row) {
is IndicatorValue -> {
listOf(
demandedAmount.toString(),
demandedUnit.toString(),
demandedProductName,
allocationAmount.toString(),
row.name,
"",
"",
"",
supplyAmount.toString(),
supply.unit.toString(),
)
}

is ProductValue -> {
listOf(
demandedAmount.toString(),
demandedUnit.toString(),
demandedProductName,
allocationAmount.toString(),
row.name,
row.fromProcessRef?.name ?: "",
row.fromProcessRef?.matchLabels?.toString() ?: "",
row.fromProcessRef?.arguments?.toString() ?: "",
supplyAmount.toString(),
supply.unit.toString(),
)
}

is FullyQualifiedSubstanceValue -> {
listOf(
demandedAmount.toString(),
demandedUnit.toString(),
demandedProductName,
allocationAmount.toString(),
row.name,
row.compartment,
row.subcompartment ?: "",
row.type.toString(),
supplyAmount.toString(),
supply.unit.toString(),
)
}

is PartiallyQualifiedSubstanceValue -> {
listOf(
demandedAmount.toString(),
demandedUnit.toString(),
demandedProductName,
allocationAmount.toString(),
row.name,
"",
"",
"",
supplyAmount.toString(),
supply.unit.toString(),
)
}
}
val impacts = controllablePorts.flatMap { col ->
val impact = analysis.getPortContribution(row, col)
val impactAmount = impact.amount.value * allocationAmount
listOf(
impactAmount.toString(),
impact.unit.toString(),
)
}
prefix.plus(impacts)
}
}


val s = StringBuilder()
CSVPrinter(s, format).printRecord(header)
echo(s.toString(), trailingNewline = false)
lines
.forEach {
s.clear()
CSVPrinter(s, format).printRecord(it)
echo(s.toString(), trailingNewline = false)
}
}

private val format = CSVFormat.DEFAULT.builder()
.setHeader()
.setSkipHeaderRecord(true)
.setRecordSeparator(System.lineSeparator())
.build()
}
42 changes: 40 additions & 2 deletions cli/src/main/kotlin/ch/kleis/lcaac/cli/cmd/Utils.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
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.evaluator.reducer.DataExpressionReducer
import ch.kleis.lcaac.core.lang.expression.*
import ch.kleis.lcaac.core.lang.value.QuantityValue
import ch.kleis.lcaac.core.lang.value.StringValue
import ch.kleis.lcaac.core.math.basic.BasicNumber
import ch.kleis.lcaac.core.math.basic.BasicOperations
import ch.kleis.lcaac.grammar.CoreMapper
Expand Down Expand Up @@ -55,3 +57,39 @@ fun smartParseQuantityWithDefaultUnit(s: String, defaultUnit: DataExpression<Bas
}
}

fun prepareArguments(
dataReducer: DataExpressionReducer<BasicNumber>,
template: EProcessTemplate<BasicNumber>,
request: Map<String, String>,
) = template.params.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 EDefaultRecordOf -> {
val dataSource = dataReducer.evalDataSource(v.dataSource)
val schema = dataSource.schema
val entries = schema.mapValues { schemaEntry ->
when (val defaultValue = schemaEntry.value) {
is QuantityValue -> request[schemaEntry.key]?.let {
smartParseQuantityWithDefaultUnit(it, defaultValue.unit.toEUnitLiteral())
} ?: defaultValue.toEQuantityScale()

is StringValue -> request[schemaEntry.key]?.let {
EStringLiteral(it)
} ?: defaultValue.toEStringLiteral()

else -> throw EvaluatorException("datasource '${dataSource.location}': column '${schemaEntry.key}': invalid default value")
}
}
ERecord(entries)
}

else -> throw EvaluatorException("$v is not a supported data expression")
}
}
41 changes: 3 additions & 38 deletions cli/src/main/kotlin/ch/kleis/lcaac/cli/csv/CsvProcessor.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
package ch.kleis.lcaac.cli.csv

import ch.kleis.lcaac.cli.cmd.smartParseQuantityWithDefaultUnit
import ch.kleis.lcaac.cli.cmd.prepareArguments
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
import ch.kleis.lcaac.core.lang.evaluator.reducer.DataExpressionReducer
import ch.kleis.lcaac.core.lang.expression.*
import ch.kleis.lcaac.core.lang.value.QuantityValue
import ch.kleis.lcaac.core.lang.value.StringValue
import ch.kleis.lcaac.core.math.basic.BasicNumber
import ch.kleis.lcaac.core.math.basic.BasicOperations
import java.io.File
Expand All @@ -28,40 +25,7 @@ class CsvProcessor(
val reqLabels = request.matchLabels
val template = 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 {
smartParseQuantityWithDefaultUnit(it, EUnitOf(v))
} ?: entry.value

is StringExpression -> request[entry.key]?.let {
EStringLiteral(it)
} ?: entry.value

is EDefaultRecordOf -> {
val dataSource = dataReducer.evalDataSource(v.dataSource)
val schema = dataSource.schema
val entries = schema.mapValues { schemaEntry ->
when (val defaultValue = schemaEntry.value) {
is QuantityValue -> request[schemaEntry.key]?.let {
smartParseQuantityWithDefaultUnit(it, defaultValue.unit.toEUnitLiteral())
} ?: defaultValue.toEQuantityScale()

is StringValue -> request[schemaEntry.key]?.let {
EStringLiteral(it)
} ?: defaultValue.toEStringLiteral()

else -> throw EvaluatorException("datasource '${dataSource.location}': column '${schemaEntry.key}': invalid default value")
}
}
ERecord(entries)
}

else -> throw EvaluatorException("$v is not a supported data expression")
}
}

val arguments = prepareArguments(dataReducer, template, request.toMap())
val trace = evaluator.trace(template, arguments)
val systemValue = trace.getSystemValue()
val entryPoint = trace.getEntryPoint()
Expand All @@ -77,4 +41,5 @@ class CsvProcessor(
)
}
}

}
4 changes: 4 additions & 0 deletions cli/src/main/kotlin/ch/kleis/lcaac/cli/csv/CsvRequest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,8 @@ class CsvRequest(
return header[name]
?.let { record[it] }
}

fun toMap(): Map<String, String> {
return header.mapValues { record[it.value] }
}
}
10 changes: 0 additions & 10 deletions cli/src/main/kotlin/ch/kleis/lcaac/cli/csv/Utils.kt

This file was deleted.

2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ javaVersion=17
gradleVersion=7.6
org.gradle.jvmargs=-Xmx4096m
lcaacGroup=ch.kleis.lcaac
lcaacVersion=1.6.0
lcaacVersion=1.6.1
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ class CoreTestMapper {
}
},
template = EProcessTemplate(
locals = ctx.variables().flatMap { it.assignment() }
.associate { it.dataRef().uid().ID().text to coreMapper.dataExpression(it.dataExpression()) },
body = EProcess(
name = processName,
products = listOf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ class CoreTestMapperTest {
// given
val content = """
test foo {
variables {
x = 1 kg
}
given {
1 kWh electricity
}
Expand Down Expand Up @@ -41,6 +44,9 @@ class CoreTestMapperTest {
)
assertEquals(
EProcessTemplate(
locals = mapOf(
"x" to EQuantityScale(BasicNumber(1.0), EDataRef("kg")),
),
body = EProcess(
name = "__test__foo",
products = listOf(
Expand Down

0 comments on commit 60f3a91

Please sign in to comment.