Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 0 additions & 14 deletions appabuild/config/lca.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,20 +99,6 @@ def parse_parameters(cls, parameters: List[dict]) -> List[ImpactModelParam]:

return parsed_parameters

@field_validator("parameters", mode="after")
@classmethod
def eval_exprs(cls, parameters: List[ImpactModelParam]) -> List[ImpactModelParam]:
exprs_set = ParamsValuesSet.build(
{param.name: param.default for param in parameters},
ImpactModelParams.from_list(parameters),
)

values = exprs_set.evaluate()
for param in parameters:
param.update_default(values[param.name])

return parameters

def dump_parameters(self) -> List[dict]:
return list(map(lambda p: p.model_dump(), self.parameters))

Expand Down
25 changes: 4 additions & 21 deletions appabuild/database/databases.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""
Contains classes to import LCI databases in Brightway project.
Initialize parameters_registry object which must be filled before Impact Model build.
"""

from __future__ import annotations
Expand All @@ -9,12 +8,11 @@
import json
import os
import re
from collections import ChainMap
from typing import Dict, List, Optional
from typing import Optional

import brightway2 as bw
import yaml
from apparun.parameters import ImpactModelParam
from apparun.parameters import ImpactModelParams
from lca_algebraic import resetParams, setForeground
from lxml.etree import XMLSyntaxError
from pydantic_core import ValidationError
Expand All @@ -25,8 +23,6 @@
from appabuild.exceptions import BwDatabaseError, SerializedDataError
from appabuild.logger import log_validation_error, logger

parameters_registry = {}


class Database:
"""
Expand Down Expand Up @@ -219,12 +215,12 @@ def __init__(self, name, path):
"""
Database.__init__(self, name, path)
self.fu_name = ""
self.parameters = {}
self.parameters = None
self.context = UserDatabaseContext(
serialized_activities=[], activities=[], database=BwDatabase(name=name)
)

def set_functional_unit(self, fu_name: str, parameters: dict):
def set_functional_unit(self, fu_name: str, parameters: ImpactModelParams):
self.fu_name = fu_name
self.parameters = parameters

Expand Down Expand Up @@ -278,7 +274,6 @@ def execute_at_startup(self):
self.find_activities_on_disk()

def execute_at_build_time(self):
self.declare_parameters()
self.import_in_project()

def import_in_project(self) -> None:
Expand Down Expand Up @@ -313,15 +308,3 @@ def import_in_project(self) -> None:
]
bw_database.write(dict(to_write_activities))
setForeground(self.name)

def declare_parameters(self) -> None:
"""
Initialize each object's parameter as a ImpactModelParam and store it in
parameters_registry object. parameters_registry is used to build Impact Model's
parameters section.
:return:
"""
for parameter in self.parameters:
parameters_registry[parameter["name"]] = ImpactModelParam.from_dict(
parameter
)
6 changes: 6 additions & 0 deletions appabuild/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,9 @@ class SerializedDataError(Exception):
"""Raised when any problem concerning yaml/json dataset is encountered."""

pass


class ParameterError(Exception):
"""Raised when any problem concerning impact model parameterization is encountered."""

pass
134 changes: 96 additions & 38 deletions appabuild/model/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from __future__ import annotations

import itertools
import logging
import os
import types
from collections import OrderedDict
Expand All @@ -28,13 +29,19 @@
_multiLCAWithCache,
_replace_fixed_params,
)
from lca_algebraic.params import _fixed_params, newEnumParam, newFloatParam
from lca_algebraic.params import (
_fixed_params,
_param_registry,
newEnumParam,
newFloatParam,
)
from pydantic import ValidationError
from sympy import Expr, simplify, symbols, sympify
from sympy.parsing.sympy_parser import parse_expr

from appabuild.config.lca import LCAConfig
from appabuild.database.databases import ForegroundDatabase, parameters_registry
from appabuild.exceptions import BwDatabaseError, BwMethodError
from appabuild.database.databases import ForegroundDatabase
from appabuild.exceptions import BwDatabaseError, BwMethodError, ParameterError
from appabuild.logger import logger

act_symbols = {} # Cache of act = > symbol
Expand Down Expand Up @@ -76,7 +83,7 @@ def __init__(
output_path: str,
metadata: Optional[ModelMetadata] = ModelMetadata(),
compile_models: bool = True,
parameters: Optional[dict] = None,
parameters: Optional[ImpactModelParams] = None,
):
"""
Initialize the model builder
Expand Down Expand Up @@ -110,20 +117,16 @@ def from_yaml(lca_config_path: str) -> ImpactModelBuilder:
lca_config = LCAConfig.from_yaml(lca_config_path)

builder = ImpactModelBuilder(
lca_config.scope.fu.database, # lca_config["scope"]["fu"]["database"],
lca_config.scope.fu.name, # lca_config["scope"]["fu"]["name"],
lca_config.scope.methods, # lca_config["scope"]["methods"],
# os.path.join(
# lca_config["outputs"]["model"]["path"],
# f"{lca_config['outputs']['model']['name']}.yaml",
# ),
lca_config.scope.fu.database,
lca_config.scope.fu.name,
lca_config.scope.methods,
os.path.join(
lca_config.model.path,
lca_config.model.name + ".yaml",
),
lca_config.model.metadata, # lca_config["outputs"]["model"]["metadata"],
lca_config.model.compile, # lca_config["outputs"]["model"]["compile"],
lca_config.model.dump_parameters(), # lca_config["outputs"]["model"]["parameters"],
lca_config.model.metadata,
lca_config.model.compile,
ImpactModelParams.from_list(lca_config.model.parameters),
)
return builder

Expand Down Expand Up @@ -183,6 +186,9 @@ def build_impact_tree_and_parameters(
method format is Appa Run method keys.
:return: root node (corresponding to the reference flow) and used parameters.
"""
# lcaa param registry can be populated if a model has already been built
_param_registry().clear()

methods_bw = [to_bw_method(MethodFullName[method]) for method in methods]
tree = ImpactTreeNode(
name=functional_unit_bw["name"],
Expand All @@ -192,7 +198,45 @@ def build_impact_tree_and_parameters(
# print("computing model to expression for %s" % model)
self.actToExpression(functional_unit_bw, tree)

# Find required parameters by inspecting symbols
# Check if each symbol corresponds to a known parameter

# TODO move that in a FloatParam method
params_in_default = [
parameter.default
for parameter in self.parameters
if parameter.type == "float"
and (
isinstance(parameter.default, str)
or isinstance(parameter.default, dict)
)
]
while (
len(
[
parameter
for parameter in params_in_default
if isinstance(parameter, dict)
]
)
> 0
):
params_in_default_str = [
parameter
for parameter in params_in_default
if isinstance(parameter, str)
]
params_in_default_dict = [
[value for value in parameter.values()]
for parameter in params_in_default
if isinstance(parameter, dict)
]
params_in_default = (
list(itertools.chain.from_iterable(params_in_default_dict))
+ params_in_default_str
)
params_in_default = [
parameter for parameter in params_in_default if isinstance(parameter, str)
] # there can be int params at this point
free_symbols = set(
list(
itertools.chain.from_iterable(
Expand All @@ -211,19 +255,29 @@ def build_impact_tree_and_parameters(
]
)
)
+ [
str(symb)
for symb in list(
itertools.chain.from_iterable(
[
parse_expr(params_in_default).free_symbols
for params_in_default in params_in_default
]
)
)
]
)

activity_symbols = set([str(symb["symbol"]) for _, symb in act_symbols.items()])

expected_parameter_symbols = free_symbols - activity_symbols

known_parameters = ImpactModelParams.from_list(parameters_registry.values())

forbidden_parameter_names = list(
itertools.chain(
*[
[
elem.name
for elem in known_parameters.find_corresponding_parameter(
for elem in self.parameters.find_corresponding_parameter(
activity_symbol, must_find_one=False
)
]
Expand All @@ -234,43 +288,47 @@ def build_impact_tree_and_parameters(

try:
if len(forbidden_parameter_names) > 0:
raise ValueError(
raise ParameterError(
f"Parameter names {forbidden_parameter_names} are forbidden as they "
f"correspond to background activities."
)
except ValueError:
logger.exception("ValueError")
raise

used_parameters = [
known_parameters.find_corresponding_parameter(expected_parameter_symbol)
for expected_parameter_symbol in expected_parameter_symbols
]
unique_used_parameters = []
[
unique_used_parameters.append(i)
for i in used_parameters
if i not in unique_used_parameters
]
unique_used_parameters = ImpactModelParams.from_list(unique_used_parameters)
except ParameterError as e:
logger.exception(e)
raise ParameterError(e)
for expected_parameter_symbol in expected_parameter_symbols:
try:
self.parameters.find_corresponding_parameter(expected_parameter_symbol)
except ValueError:
e = (
f"ValueError : {expected_parameter_symbol} is required in the impact"
f" model but is unknown in the config. Please check in the LCA "
f"config."
)
logger.error(e)
raise ParameterError(e)

# Declare used parameters in conf file as a lca_algebraic parameter to enable
# model building (will not be used afterwards)
for parameter in unique_used_parameters:

for parameter in self.parameters:
if parameter.name in _param_registry().keys():
e = f"Parameter {parameter.name} already in lcaa registry."
logging.error(e)
raise ParameterError(e)
if isinstance(parameter, FloatParam):
newFloatParam(
name=parameter.name,
default=parameter.default,
save=False,
dbname="",
dbname=self.user_database_name,
min=0.0,
)
if isinstance(parameter, EnumParam):
newEnumParam(
name=parameter.name,
values=parameter.weights,
default=parameter.default,
dbname="",
dbname=self.user_database_name,
)

# Create dummy reference to biosphere
Expand Down Expand Up @@ -299,7 +357,7 @@ def build_impact_tree_and_parameters(
}
)
node.direct_impacts[method] = model_expr.xreplace(sub)
return tree, unique_used_parameters
return tree, self.parameters

@staticmethod
@with_db_context
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ dependencies = [
"kaleido",
"tqdm",
"ruamel.yaml",
"apparun==0.3.6",
"apparun==0.3.7",
"typer==0.15.1",
"ipython>=7.6.0,<=8.34.0",
"mermaid-py==0.7.1"
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ aenum
kaleido
tqdm
ruamel.yaml
apparun==0.3.6
apparun==0.3.7
ipython>=7.6.0,<=8.34.0
pre-commit
hatchling
Expand Down
16 changes: 11 additions & 5 deletions samples/conf/nvidia_ai_gpu_chip_lca_conf.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,17 @@ model:
report:
link: https://appalca.github.io/
description: "A mock example of Appa LCA's impact model corresponding to a fictive AI chip accelerator based on NVIDIA GPU."
date: 03/11/2023
date: 07/10/2025
version: "1"
license: proprietary
appabuild_version: "0.2"
appabuild_version: "0.3.6"
parameters:
- name: cuda_core
type: float
default: 512
default:
architecture:
Maxwell: 1344
Pascal: 1280
min: 256
max: 4096
- name: architecture
Expand All @@ -44,14 +47,17 @@ model:
EU: 1
- name: energy_per_inference
type: float
default: 110.0
default:
architecture: #we model energy by inference using a model of the TDP function of the number of cuda cores. We suppose that TDP power corresponds to running inferences at 90fps.
Maxwell: "0.0878*cuda_core*1000/(90*3600)"
Pascal: "0.0679*cuda_core*1000/(90*3600)"
pm_perc: 0.2
- name: lifespan
type: float
default: 2.0
pm: 1
- name: inference_per_day
type: float
default: 86400000
default: "30*3600*8" #30fps 8 hours a day
min: 0
max: 86400000
Loading