Skip to content

Commit

Permalink
Merge pull request #9 from kleis-technology/feature/improve-ui
Browse files Browse the repository at this point in the history
cli: refactored flags, added argument override
  • Loading branch information
pevab authored Jan 19, 2024
2 parents 8b16bd3 + 7b66d3d commit e8467e8
Show file tree
Hide file tree
Showing 8 changed files with 207 additions and 51 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ electricity,1.0,kWh,5.4,kg

You can also run multiple assessments with an external data csv file providing values for the process parameters.
```bash
lcaac assess "electricity_mix" --data params.csv
lcaac assess "electricity_mix" --file params.csv
```

### Tests
Expand Down
2 changes: 1 addition & 1 deletion cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ lcaac assess "electricity_mix"

```bash
cd $GIT_ROOT/cli/samples
lcaac assess "electricity_mix" --data params.csv
lcaac assess "electricity_mix" --file params.csv
```

## Run tests
Expand Down
43 changes: 36 additions & 7 deletions cli/src/main/kotlin/ch/kleis/lcaac/cli/cmd/AssessCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,29 @@ import java.io.File
@Suppress("MemberVisibilityCanBePrivate")
class AssessCommand : CliktCommand(name = "assess", help = "Returns the unitary impacts of a process in CSV format") {
val name: String by argument().help("Process name")
val labels: Map<String, String> by option("-l", "--label").associate()
val root: File by option("-r", "--root").file(canBeFile = false).default(File(".")).help("Root folder")
val data: File? by option("-d", "--data").file(canBeDir = false).help("CSV file with parameter values")
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()
val path: File by option("-p", "--path").file(canBeFile = false).default(File(".")).help("Path to root folder.")
val file: File? by option("-f", "--file").file(canBeDir = false)
.help("""
CSV file with parameter values.
Example: `lcaac assess <process name> -f params.csv`.
""".trimIndent())
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 files = lcaFiles(root)
val files = lcaFiles(path)
val symbolTable = Loader(BasicOperations).load(files, listOf(LoaderOption.WITH_PRELUDE))
val processor = CsvProcessor(symbolTable)
val iterator = loadRequests()
Expand All @@ -45,12 +62,24 @@ class AssessCommand : CliktCommand(name = "assess", help = "Returns the unitary
}

private fun loadRequests(): Iterator<CsvRequest> {
return data?.let { loadRequestsFrom(it) }
?: listOf(CsvRequest(name, labels, emptyMap(), emptyList())).iterator()
return file?.let { loadRequestsFrom(it) }
?: listOf(defaultRequest()).iterator()
}

private fun loadRequestsFrom(file: File): Iterator<CsvRequest> {
val reader = CsvRequestReader(name, labels, file.inputStream())
val reader = CsvRequestReader(name, labels, file.inputStream(), arguments)
return reader.iterator()
}

private fun defaultRequest(): CsvRequest {
val pairs = arguments.toList()
val header = pairs.mapIndexed { index, pair -> pair.first to index }.toMap()
val record = pairs.map { it.second }
return CsvRequest(
name,
labels,
header,
record,
)
}
}
38 changes: 38 additions & 0 deletions cli/src/main/kotlin/ch/kleis/lcaac/cli/cmd/Utils.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
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
import ch.kleis.lcaac.grammar.parser.LcaLangLexer
import ch.kleis.lcaac.grammar.parser.LcaLangParser
import org.antlr.v4.runtime.CharStreams
import org.antlr.v4.runtime.CommonTokenStream
import java.io.File
import java.io.InputStream
import java.lang.Double.parseDouble
import java.nio.file.Files
import kotlin.io.path.isRegularFile

Expand All @@ -24,3 +33,32 @@ private fun lcaFile(inputStream: InputStream): LcaLangParser.LcaFileContext {
val parser = LcaLangParser(tokens)
return parser.lcaFile()
}

fun parseQuantityWithDefaultUnit(s: String, defaultUnit: DataExpression<BasicNumber>): DataExpression<BasicNumber> {
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)
}
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")
}
}
else -> throw EvaluatorException("'$s' is not a valid quantity")
}

}

52 changes: 27 additions & 25 deletions cli/src/main/kotlin/ch/kleis/lcaac/cli/csv/CsvProcessor.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
package ch.kleis.lcaac.cli.csv

import ch.kleis.lcaac.cli.cmd.parseQuantityWithDefaultUnit
import ch.kleis.lcaac.core.assessment.ContributionAnalysisProgram
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.expression.*
import ch.kleis.lcaac.core.lang.expression.EStringLiteral
import ch.kleis.lcaac.core.lang.expression.EUnitOf
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.lang.Double.parseDouble

class CsvProcessor(
private val symbolTable: SymbolTable<BasicNumber>,
private val symbolTable: SymbolTable<BasicNumber>,
) {
private val ops = BasicOperations
private val evaluator = Evaluator(symbolTable, ops)
Expand All @@ -19,39 +22,38 @@ class CsvProcessor(
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 {
val amount = parseDouble(it)
EQuantityScale(ops.pure(amount), EUnitOf(v))
} ?: entry.value
.mapValues { entry ->
when (val v = entry.value) {
is QuantityExpression<*> -> request[entry.key]?.let {
parseQuantityWithDefaultUnit(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()
val entryPoint = trace.getEntryPoint()
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,
)
}
}
}
34 changes: 22 additions & 12 deletions cli/src/main/kotlin/ch/kleis/lcaac/cli/csv/CsvRequestReader.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,42 @@ import org.apache.commons.csv.CSVParser
import java.io.InputStream

class CsvRequestReader(
private val processName: String,
private val matchLabels: Map<String, String>,
private val inputStream: InputStream,
private val processName: String,
private val matchLabels: Map<String, String>,
private val inputStream: InputStream,
private val overrideArguments: Map<String, String> = emptyMap(),
) {
private val format = CSVFormat.DEFAULT.builder()
.setHeader()
.setSkipHeaderRecord(true)
.build()
.setHeader()
.setSkipHeaderRecord(true)
.build()

fun iterator(): Iterator<CsvRequest> {
return object: Iterator<CsvRequest> {
return object : Iterator<CsvRequest> {
private val parser = CSVParser(inputStream.reader(), format)
private val header = parser.headerMap
private val augmentedHeader = header.plus(
overrideArguments.keys
.filter { !header.containsKey(it) }
.mapIndexed { index, k -> k to (header.size + index) }
)
private val iterator = parser.iterator()

override fun hasNext(): Boolean {
return iterator.hasNext()
}

override fun next(): CsvRequest {
val record = iterator.next()
val record = iterator.next().toMap()
.plus(overrideArguments)
.toList()
.sortedBy { augmentedHeader[it.first] }
.map { it.second }
return CsvRequest(
processName,
matchLabels,
header,
record.toList()
processName,
matchLabels,
augmentedHeader,
record,
)
}
}
Expand Down
73 changes: 73 additions & 0 deletions cli/src/test/kotlin/ch/kleis/lcaac/cli/cmd/UtilsKtTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
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.EQuantityScale
import ch.kleis.lcaac.core.math.basic.BasicNumber
import ch.kleis.lcaac.core.prelude.Prelude
import org.junit.jupiter.api.assertThrows
import kotlin.test.Test
import kotlin.test.assertEquals


class UtilsKtTest {

@Test
fun parseQuantityWithDefaultUnit_invalidExpression() {
// given
val s = "a@bc"
val defaultUnit = Prelude.unitMap<BasicNumber>()["kg"]!!

// when/then
val actual = assertThrows<EvaluatorException> { parseQuantityWithDefaultUnit(s, defaultUnit) }
assertEquals("'a@bc' is not a valid quantity", actual.message)
}

@Test
fun parseQuantityWithDefaultUnit_invalidExpression_multipleParts() {
// given
val s = "12 3 4"
val defaultUnit = Prelude.unitMap<BasicNumber>()["kg"]!!

// when/then
val actual = assertThrows<EvaluatorException> { parseQuantityWithDefaultUnit(s, defaultUnit) }
assertEquals("'12 3 4' is not a valid quantity", actual.message)
}

@Test
fun parseQuantityWithDefaultUnit_invalidExpression_invalidUnit() {
// given
val s = "12 $3"
val defaultUnit = Prelude.unitMap<BasicNumber>()["kg"]!!

// when/then
val actual = assertThrows<EvaluatorException> { parseQuantityWithDefaultUnit(s, defaultUnit) }
assertEquals("'12 \$3' is not a valid quantity", actual.message)
}

@Test
fun parseQuantityWithDefaultUnit_whenNumber() {
// given
val s = "12.0"
val defaultUnit = Prelude.unitMap<BasicNumber>()["kg"]!!

// when
val actual = parseQuantityWithDefaultUnit(s, defaultUnit)

// then
assertEquals(EQuantityScale(BasicNumber(12.0), defaultUnit), actual)
}

@Test
fun parseQuantityWithDefaultUnit_whenExpression() {
// given
val s = "12.0 kg"
val defaultUnit = Prelude.unitMap<BasicNumber>()["kg"]!!

// when
val actual = parseQuantityWithDefaultUnit(s, defaultUnit)

// then
assertEquals(EQuantityScale(BasicNumber(12.0), EDataRef("kg")), actual)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,13 @@ class CsvRequestReaderTest {
ch,2020,0.20,0.40,0.40
""".trimIndent()
val reader = CsvRequestReader(
"process",
mapOf("label" to "value"),
content.byteInputStream()
"process",
mapOf("label" to "value"),
content.byteInputStream(),
mapOf(
"year" to "2024",
"foo" to "bar",
)
)

// when
Expand All @@ -26,8 +30,8 @@ class CsvRequestReaderTest {
// then
assertEquals(actual.processName, "process")
assertEquals(actual.matchLabels, mapOf("label" to "value"))
assertEquals(actual.columns(), listOf("country", "year", "fossil", "nuclear", "hydro"))
assertEquals(actual.arguments(), listOf("ch", "2020", "0.20", "0.40", "0.40"))
assertEquals(actual.columns(), listOf("country", "year", "fossil", "nuclear", "hydro", "foo"))
assertEquals(actual.arguments(), listOf("ch", "2024", "0.20", "0.40", "0.40", "bar"))
assertFalse { iterator.hasNext() }
}
}

0 comments on commit e8467e8

Please sign in to comment.