Skip to content
2 changes: 1 addition & 1 deletion cli/src/main/resources/META-INF/lcaac.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#Thu Jul 24 10:18:57 CEST 2025
#Thu Aug 14 10:41:08 CEST 2025
author=Kleis Technology
description=LCA as Code CLI
version=1.7.13
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import ch.kleis.lcaac.core.lang.evaluator.protocol.CachedOracle
import ch.kleis.lcaac.core.lang.evaluator.protocol.Learner
import ch.kleis.lcaac.core.lang.expression.*
import ch.kleis.lcaac.core.lang.register.ProcessKey
import ch.kleis.lcaac.core.math.Operations
import ch.kleis.lcaac.core.math.QuantityOperations
import org.slf4j.LoggerFactory

class Evaluator<Q>(
class Evaluator<Q, M>(
private val symbolTable: SymbolTable<Q>,
private val ops: QuantityOperations<Q>,
private val ops: Operations<Q, M>,
private val sourceOps: DataSourceOperations<Q>,
) {
@Suppress("PrivatePropertyName")
Expand All @@ -35,7 +36,7 @@ class Evaluator<Q>(
}
}

fun with(template: EProcessTemplate<Q>): Evaluator<Q> {
fun with(template: EProcessTemplate<Q>): Evaluator<Q, M> {
val processKey = ProcessKey(template.body.name)
if (symbolTable.processTemplates[processKey] != null) throw IllegalStateException("Process ${template.body.name} already exists")
val st = this.symbolTable.copy(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,18 @@ import ch.kleis.lcaac.core.lang.SymbolTable
import ch.kleis.lcaac.core.lang.evaluator.step.CompleteTerminals
import ch.kleis.lcaac.core.lang.evaluator.step.Reduce
import ch.kleis.lcaac.core.lang.expression.EProcess
import ch.kleis.lcaac.core.lang.expression.EProcessTemplateApplication
import ch.kleis.lcaac.core.lang.resolver.ProcessResolver
import ch.kleis.lcaac.core.lang.expression.ProcessAnnotation
import ch.kleis.lcaac.core.lang.resolver.BareProcessResolver
import ch.kleis.lcaac.core.lang.resolver.CachedProcessResolver
import ch.kleis.lcaac.core.lang.resolver.ProcessTemplateResolver
import ch.kleis.lcaac.core.lang.resolver.SubstanceCharacterizationResolver
import ch.kleis.lcaac.core.math.QuantityOperations
import ch.kleis.lcaac.core.math.Operations
import com.mayakapps.kache.InMemoryKache
import com.mayakapps.kache.KacheStrategy
import com.mayakapps.kache.ObjectKache
import kotlinx.coroutines.runBlocking

interface Oracle<Q> {
interface Oracle<Q, M> {
fun answer(ports: Set<Request<Q>>): Set<Response<Q>> {
return ports.mapNotNull {
answerRequest(it)
Expand All @@ -23,15 +25,15 @@ interface Oracle<Q> {
fun answerRequest(request: Request<Q>): Response<Q>?
}

class CachedOracle<Q>(
private val inner: Oracle<Q>,
class CachedOracle<Q, M>(
private val inner: Oracle<Q, M>,
private val cache: ObjectKache<Request<Q>, Response<Q>> = InMemoryKache(maxSize = 1024) {
strategy = KacheStrategy.LRU
}
) : Oracle<Q> {
) : Oracle<Q, M> {
constructor(
symbolTable: SymbolTable<Q>,
ops: QuantityOperations<Q>,
ops: Operations<Q, M>,
sourceOps: DataSourceOperations<Q>,
cache: ObjectKache<Request<Q>, Response<Q>> = InMemoryKache(maxSize = 1024) {
strategy = KacheStrategy.LRU
Expand All @@ -50,14 +52,14 @@ class CachedOracle<Q>(
}
}

class BareOracle<Q>(
class BareOracle<Q, M>(
val symbolTable: SymbolTable<Q>,
val ops: QuantityOperations<Q>,
sourceOps: DataSourceOperations<Q>,
): Oracle<Q> {
val ops: Operations<Q, M>,
val sourceOps: DataSourceOperations<Q>,
): Oracle<Q, M> {
private val reduceDataExpressions = Reduce(symbolTable, ops, sourceOps)
private val completeTerminals = CompleteTerminals(ops)
private val processResolver = ProcessResolver(symbolTable)
private val processTemplateResolver = ProcessTemplateResolver(symbolTable)
private val substanceCharacterizationResolver = SubstanceCharacterizationResolver(symbolTable)

override fun answerRequest(request: Request<Q>): Response<Q>? {
Expand All @@ -67,15 +69,19 @@ class BareOracle<Q>(
}
}


private fun answerProductRequest(request: ProductRequest<Q>): ProductResponse<Q>? {
val spec = request.value
val template = processResolver.resolve(spec) ?: return null
val arguments = template.params
.plus(spec.fromProcess?.arguments ?: emptyMap())
val expression = EProcessTemplateApplication(template, arguments)
val process = expression
.let(reduceDataExpressions::apply)
.let(completeTerminals::apply)
val template = processTemplateResolver.resolve(spec) ?: return null

val processResolver = if (template.annotations.contains(ProcessAnnotation.CACHED)) {
CachedProcessResolver(symbolTable, ops, sourceOps)
} else {
BareProcessResolver(symbolTable, ops, sourceOps)
}

val process = processResolver.resolve(template, spec)

val selectedPortIndex = indexOf(request.value.name, process)
return ProductResponse(request.address, process, selectedPortIndex)
}
Expand All @@ -92,5 +98,4 @@ class BareOracle<Q>(
private fun indexOf(productName: String, process: EProcess<Q>): Int {
return process.products.indexOfFirst { it.product.name == productName }
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package ch.kleis.lcaac.core.lang.expression

import ch.kleis.lcaac.core.lang.value.*

object EMapper {
fun <Q> toDataExpression(value: DataValue<Q>): DataExpression<Q> {
return when (value) {
is QuantityValue -> value.toEQuantityScale()
is RecordValue -> value.toERecord()
is StringValue -> value.toEStringLiteral()
}
}

fun <Q> toFromProcess(value: FromProcessRefValue<Q>): FromProcess<Q> {
val labels = MatchLabels(value.matchLabels.map { it.key to it.value.toEStringLiteral() }.toMap())
val arguments: Map<String, DataExpression<Q>> = value.arguments.map { it.key to toDataExpression(it.value) }.toMap()
return FromProcess(value.name, labels, arguments)
}

fun <Q> toETechnoExchange(quantity: QuantityValue<Q>, product: ProductValue<Q>): ETechnoExchange<Q> {
return ETechnoExchange(
quantity = quantity.toEQuantityScale(),
product = EProductSpec(
product.name,
product.referenceUnit.toEUnitLiteral(),
product.fromProcessRef?.let { toFromProcess(it) }
)
)
}

fun <Q> toETechnoExchange(value: TechnoExchangeValue<Q>): ETechnoExchange<Q> {
return ETechnoExchange(
quantity = value.quantity.toEQuantityScale(),
product = EProductSpec(
value.product.name,
value.product.referenceUnit.toEUnitLiteral(),
value.product.fromProcessRef?.let { toFromProcess(it) }
),
allocation = value.allocation?.toEQuantityScale()
)
}

fun <Q> toEBioExchange(quantity: QuantityValue<Q>, substance: SubstanceValue<Q>): EBioExchange<Q> {
return EBioExchange(
quantity = quantity.toEQuantityScale(),
substance = when (substance) {
is FullyQualifiedSubstanceValue -> ESubstanceSpec(
name = substance.getShortName(),
displayName = substance.getDisplayName(),
type = substance.type,
compartment = substance.compartment,
subCompartment = substance.subcompartment,
referenceUnit = substance.referenceUnit.toEUnitLiteral()
)
is PartiallyQualifiedSubstanceValue -> ESubstanceSpec(
name = substance.getShortName(),
displayName = substance.getDisplayName(),
referenceUnit = substance.referenceUnit.toEUnitLiteral()
)
}
)
}

fun <Q> toEImpact(quantity: QuantityValue<Q>, value: IndicatorValue<Q>): EImpact<Q> {
return EImpact(
quantity = quantity.toEQuantityScale(),
indicator = EIndicatorSpec(value.name, value.referenceUnit.toEUnitLiteral())
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,16 @@ sealed interface ProcessTemplateExpression<Q> {
companion object
}

enum class ProcessAnnotation {
CACHED
}

@optics
data class EProcessTemplate<Q>(
val params: Map<String, DataExpression<Q>> = emptyMap(),
val locals: Map<String, DataExpression<Q>> = emptyMap(),
val body: EProcess<Q>,
val annotations: Set<ProcessAnnotation> = emptySet(),
) : ProcessTemplateExpression<Q> {
companion object
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,38 +1,97 @@
package ch.kleis.lcaac.core.lang.resolver

import ch.kleis.lcaac.core.assessment.AnalysisProgram
import ch.kleis.lcaac.core.datasource.DataSourceOperations
import ch.kleis.lcaac.core.lang.SymbolTable
import ch.kleis.lcaac.core.lang.evaluator.EvaluatorException
import ch.kleis.lcaac.core.lang.expression.EProcessTemplate
import ch.kleis.lcaac.core.lang.expression.EProductSpec
import ch.kleis.lcaac.core.lang.expression.EStringLiteral

class ProcessResolver<Q>(
private val symbolTable: SymbolTable<Q>
) {
fun resolve(spec: EProductSpec<Q>): EProcessTemplate<Q>? {
if (spec.fromProcess == null) {
val matches = symbolTable.getAllTemplatesByProductName(spec.name)
return when (matches.size) {
0 -> null
1 -> matches.first()
else -> throw EvaluatorException("more than one processes found providing ${spec.name}")
}
import ch.kleis.lcaac.core.lang.evaluator.EvaluationTrace
import ch.kleis.lcaac.core.lang.evaluator.Evaluator
import ch.kleis.lcaac.core.lang.evaluator.step.CompleteTerminals
import ch.kleis.lcaac.core.lang.evaluator.step.Reduce
import ch.kleis.lcaac.core.lang.expression.*
import ch.kleis.lcaac.core.lang.register.ProcessKey
import ch.kleis.lcaac.core.lang.value.MatrixColumnIndex
import ch.kleis.lcaac.core.lang.value.QuantityValue
import ch.kleis.lcaac.core.lang.value.QuantityValueOperations
import ch.kleis.lcaac.core.lang.value.TechnoExchangeValue
import ch.kleis.lcaac.core.math.Operations
import ch.kleis.lcaac.core.matrix.ImpactFactorMatrix

interface ProcessResolver<Q, M> {
fun resolve(template: EProcessTemplate<Q>, spec: EProductSpec<Q>): EProcess<Q>
}

class CachedProcessResolver<Q, M>(
val symbolTable: SymbolTable<Q>,
val ops: Operations<Q, M>,
val sourceOps: DataSourceOperations<Q>,
) : ProcessResolver<Q, M> {
override fun resolve(template: EProcessTemplate<Q>, spec: EProductSpec<Q>): EProcess<Q> {
val trace = getTrace(template, spec)
val entryPoint = trace.getEntryPoint()
val analysis = AnalysisProgram(trace.getSystemValue(), entryPoint, ops).run()
val inputQuantity = inputQuantityAnalysis(entryPoint.products, analysis.impactFactors)

val inputs = analysis.impactFactors.getInputProducts().map {
EMapper.toETechnoExchange(inputQuantity(it), it)
}

val name = spec.fromProcess.name
val labels = spec.fromProcess.matchLabels.elements.mapValues {
when (val v = it.value) {
is EStringLiteral -> v.value
else -> throw EvaluatorException("$v is not a valid label value")
}
}
return symbolTable.getTemplate(name, labels)?.let { candidate ->
val providedProducts = candidate.body.products.map { it.product.name }
if (!providedProducts.contains(spec.name)) {
val s = if (labels.isEmpty()) name else "$name$labels"
throw EvaluatorException("no process '$s' providing '${spec.name}' found")
val biosphere = analysis.impactFactors.getSubstances().map {
EMapper.toEBioExchange(inputQuantity(it), it)
}

val impacts = analysis.impactFactors.getIndicators().map {
EMapper.toEImpact(inputQuantity(it), it)
}

return template.body.copy(
products = entryPoint.products.map { EMapper.toETechnoExchange(it)},
inputs = inputs.map { ETechnoBlockEntry(it) },
biosphere = biosphere.map { EBioBlockEntry(it) },
impacts = impacts.map { EImpactBlockEntry(it)}
)
}

private fun getTrace(template: EProcessTemplate<Q>, spec: EProductSpec<Q>): EvaluationTrace<Q> {
val arguments = template.params.plus(spec.fromProcess?.arguments ?: emptyMap())
val newAnnotations = template.annotations.filter { it != ProcessAnnotation.CACHED }.toSet()
val newTemplate = template.copy(annotations = newAnnotations)

val newSymbolTable = symbolTable.copy(processTemplates = symbolTable.processTemplates.override(
ProcessKey(newTemplate.body.name, newTemplate.body.labels.mapValues { it.value.value }),
newTemplate
))
val evaluator = Evaluator(newSymbolTable, ops, sourceOps)

return evaluator.trace(newTemplate, arguments)
}

private fun inputQuantityAnalysis(
products: List<TechnoExchangeValue<Q>>,
impactFactors: ImpactFactorMatrix<Q, M>
): (MatrixColumnIndex<Q>) -> QuantityValue<Q> {
return { inputPort: MatrixColumnIndex<Q> ->
with(QuantityValueOperations(ops)) {
products
.map { impactFactors.characterizationFactor(it.port(), inputPort) * it.quantity }
.reduce { a, b -> a + b }
}
candidate
}
}
}

class BareProcessResolver<Q, M>(
val symbolTable: SymbolTable<Q>,
val ops: Operations<Q, M>,
val sourceOps: DataSourceOperations<Q>,
) : ProcessResolver<Q, M> {
private val reduceDataExpressions = Reduce(symbolTable, ops, sourceOps)
private val completeTerminals = CompleteTerminals(ops)

override fun resolve(template: EProcessTemplate<Q>, spec: EProductSpec<Q>): EProcess<Q> {
val arguments = template.params.plus(spec.fromProcess?.arguments ?: emptyMap())
val expression = EProcessTemplateApplication(template, arguments)
return expression
.let(reduceDataExpressions::apply)
.let(completeTerminals::apply)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package ch.kleis.lcaac.core.lang.resolver

import ch.kleis.lcaac.core.lang.SymbolTable
import ch.kleis.lcaac.core.lang.evaluator.EvaluatorException
import ch.kleis.lcaac.core.lang.expression.EProcessTemplate
import ch.kleis.lcaac.core.lang.expression.EProductSpec
import ch.kleis.lcaac.core.lang.expression.EStringLiteral

class ProcessTemplateResolver<Q>(
private val symbolTable: SymbolTable<Q>
) {
fun resolve(spec: EProductSpec<Q>): EProcessTemplate<Q>? {
if (spec.fromProcess == null) {
val matches = symbolTable.getAllTemplatesByProductName(spec.name)
return when (matches.size) {
0 -> null
1 -> matches.first()
else -> throw EvaluatorException("more than one processes found providing ${spec.name}")
}
}

val name = spec.fromProcess.name
val labels = spec.fromProcess.matchLabels.elements.mapValues {
when (val v = it.value) {
is EStringLiteral -> v.value
else -> throw EvaluatorException("$v is not a valid label value")
}
}
return symbolTable.getTemplate(name, labels)?.let { candidate ->
val providedProducts = candidate.body.products.map { it.product.name }
if (!providedProducts.contains(spec.name)) {
val s = if (labels.isEmpty()) name else "$name$labels"
throw EvaluatorException("no process '$s' providing '${spec.name}' found")
}
candidate
}
}
}
Loading