Skip to content

Commit eb127f3

Browse files
committed
(fix) Do not longer evaluate params at build time. (#40)
* (fix) Do not longer evaluate params at build time. Fix sample and end to end test data. Update end to end test to run the built impact model. * (chore) Bump apparun requirement version * (fix) Use approx instead of equal in end to end test * (update) Change parameters declaration to ensure no persistence between runs. Add two tests to detect unknown parameters in default values.
1 parent dca185e commit eb127f3

22 files changed

Lines changed: 380 additions & 140 deletions

appabuild/config/lca.py

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -99,20 +99,6 @@ def parse_parameters(cls, parameters: List[dict]) -> List[ImpactModelParam]:
9999

100100
return parsed_parameters
101101

102-
@field_validator("parameters", mode="after")
103-
@classmethod
104-
def eval_exprs(cls, parameters: List[ImpactModelParam]) -> List[ImpactModelParam]:
105-
exprs_set = ParamsValuesSet.build(
106-
{param.name: param.default for param in parameters},
107-
ImpactModelParams.from_list(parameters),
108-
)
109-
110-
values = exprs_set.evaluate()
111-
for param in parameters:
112-
param.update_default(values[param.name])
113-
114-
return parameters
115-
116102
def dump_parameters(self) -> List[dict]:
117103
return list(map(lambda p: p.model_dump(), self.parameters))
118104

appabuild/database/databases.py

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"""
22
Contains classes to import LCI databases in Brightway project.
3-
Initialize parameters_registry object which must be filled before Impact Model build.
43
"""
54

65
from __future__ import annotations
@@ -9,12 +8,11 @@
98
import json
109
import os
1110
import re
12-
from collections import ChainMap
13-
from typing import Dict, List, Optional
11+
from typing import Optional
1412

1513
import brightway2 as bw
1614
import yaml
17-
from apparun.parameters import ImpactModelParam
15+
from apparun.parameters import ImpactModelParams
1816
from lca_algebraic import resetParams, setForeground
1917
from lxml.etree import XMLSyntaxError
2018
from pydantic_core import ValidationError
@@ -25,8 +23,6 @@
2523
from appabuild.exceptions import BwDatabaseError, SerializedDataError
2624
from appabuild.logger import log_validation_error, logger
2725

28-
parameters_registry = {}
29-
3026

3127
class Database:
3228
"""
@@ -219,12 +215,12 @@ def __init__(self, name, path):
219215
"""
220216
Database.__init__(self, name, path)
221217
self.fu_name = ""
222-
self.parameters = {}
218+
self.parameters = None
223219
self.context = UserDatabaseContext(
224220
serialized_activities=[], activities=[], database=BwDatabase(name=name)
225221
)
226222

227-
def set_functional_unit(self, fu_name: str, parameters: dict):
223+
def set_functional_unit(self, fu_name: str, parameters: ImpactModelParams):
228224
self.fu_name = fu_name
229225
self.parameters = parameters
230226

@@ -278,7 +274,6 @@ def execute_at_startup(self):
278274
self.find_activities_on_disk()
279275

280276
def execute_at_build_time(self):
281-
self.declare_parameters()
282277
self.import_in_project()
283278

284279
def import_in_project(self) -> None:
@@ -313,15 +308,3 @@ def import_in_project(self) -> None:
313308
]
314309
bw_database.write(dict(to_write_activities))
315310
setForeground(self.name)
316-
317-
def declare_parameters(self) -> None:
318-
"""
319-
Initialize each object's parameter as a ImpactModelParam and store it in
320-
parameters_registry object. parameters_registry is used to build Impact Model's
321-
parameters section.
322-
:return:
323-
"""
324-
for parameter in self.parameters:
325-
parameters_registry[parameter["name"]] = ImpactModelParam.from_dict(
326-
parameter
327-
)

appabuild/exceptions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,9 @@ class SerializedDataError(Exception):
1717
"""Raised when any problem concerning yaml/json dataset is encountered."""
1818

1919
pass
20+
21+
22+
class ParameterError(Exception):
23+
"""Raised when any problem concerning impact model parameterization is encountered."""
24+
25+
pass

appabuild/model/builder.py

Lines changed: 96 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from __future__ import annotations
77

88
import itertools
9+
import logging
910
import os
1011
import types
1112
from collections import OrderedDict
@@ -28,13 +29,19 @@
2829
_multiLCAWithCache,
2930
_replace_fixed_params,
3031
)
31-
from lca_algebraic.params import _fixed_params, newEnumParam, newFloatParam
32+
from lca_algebraic.params import (
33+
_fixed_params,
34+
_param_registry,
35+
newEnumParam,
36+
newFloatParam,
37+
)
3238
from pydantic import ValidationError
3339
from sympy import Expr, simplify, symbols, sympify
40+
from sympy.parsing.sympy_parser import parse_expr
3441

3542
from appabuild.config.lca import LCAConfig
36-
from appabuild.database.databases import ForegroundDatabase, parameters_registry
37-
from appabuild.exceptions import BwDatabaseError, BwMethodError
43+
from appabuild.database.databases import ForegroundDatabase
44+
from appabuild.exceptions import BwDatabaseError, BwMethodError, ParameterError
3845
from appabuild.logger import logger
3946

4047
act_symbols = {} # Cache of act = > symbol
@@ -76,7 +83,7 @@ def __init__(
7683
output_path: str,
7784
metadata: Optional[ModelMetadata] = ModelMetadata(),
7885
compile_models: bool = True,
79-
parameters: Optional[dict] = None,
86+
parameters: Optional[ImpactModelParams] = None,
8087
):
8188
"""
8289
Initialize the model builder
@@ -110,20 +117,16 @@ def from_yaml(lca_config_path: str) -> ImpactModelBuilder:
110117
lca_config = LCAConfig.from_yaml(lca_config_path)
111118

112119
builder = ImpactModelBuilder(
113-
lca_config.scope.fu.database, # lca_config["scope"]["fu"]["database"],
114-
lca_config.scope.fu.name, # lca_config["scope"]["fu"]["name"],
115-
lca_config.scope.methods, # lca_config["scope"]["methods"],
116-
# os.path.join(
117-
# lca_config["outputs"]["model"]["path"],
118-
# f"{lca_config['outputs']['model']['name']}.yaml",
119-
# ),
120+
lca_config.scope.fu.database,
121+
lca_config.scope.fu.name,
122+
lca_config.scope.methods,
120123
os.path.join(
121124
lca_config.model.path,
122125
lca_config.model.name + ".yaml",
123126
),
124-
lca_config.model.metadata, # lca_config["outputs"]["model"]["metadata"],
125-
lca_config.model.compile, # lca_config["outputs"]["model"]["compile"],
126-
lca_config.model.dump_parameters(), # lca_config["outputs"]["model"]["parameters"],
127+
lca_config.model.metadata,
128+
lca_config.model.compile,
129+
ImpactModelParams.from_list(lca_config.model.parameters),
127130
)
128131
return builder
129132

@@ -183,6 +186,9 @@ def build_impact_tree_and_parameters(
183186
method format is Appa Run method keys.
184187
:return: root node (corresponding to the reference flow) and used parameters.
185188
"""
189+
# lcaa param registry can be populated if a model has already been built
190+
_param_registry().clear()
191+
186192
methods_bw = [to_bw_method(MethodFullName[method]) for method in methods]
187193
tree = ImpactTreeNode(
188194
name=functional_unit_bw["name"],
@@ -192,7 +198,45 @@ def build_impact_tree_and_parameters(
192198
# print("computing model to expression for %s" % model)
193199
self.actToExpression(functional_unit_bw, tree)
194200

195-
# Find required parameters by inspecting symbols
201+
# Check if each symbol corresponds to a known parameter
202+
203+
# TODO move that in a FloatParam method
204+
params_in_default = [
205+
parameter.default
206+
for parameter in self.parameters
207+
if parameter.type == "float"
208+
and (
209+
isinstance(parameter.default, str)
210+
or isinstance(parameter.default, dict)
211+
)
212+
]
213+
while (
214+
len(
215+
[
216+
parameter
217+
for parameter in params_in_default
218+
if isinstance(parameter, dict)
219+
]
220+
)
221+
> 0
222+
):
223+
params_in_default_str = [
224+
parameter
225+
for parameter in params_in_default
226+
if isinstance(parameter, str)
227+
]
228+
params_in_default_dict = [
229+
[value for value in parameter.values()]
230+
for parameter in params_in_default
231+
if isinstance(parameter, dict)
232+
]
233+
params_in_default = (
234+
list(itertools.chain.from_iterable(params_in_default_dict))
235+
+ params_in_default_str
236+
)
237+
params_in_default = [
238+
parameter for parameter in params_in_default if isinstance(parameter, str)
239+
] # there can be int params at this point
196240
free_symbols = set(
197241
list(
198242
itertools.chain.from_iterable(
@@ -211,19 +255,29 @@ def build_impact_tree_and_parameters(
211255
]
212256
)
213257
)
258+
+ [
259+
str(symb)
260+
for symb in list(
261+
itertools.chain.from_iterable(
262+
[
263+
parse_expr(params_in_default).free_symbols
264+
for params_in_default in params_in_default
265+
]
266+
)
267+
)
268+
]
214269
)
270+
215271
activity_symbols = set([str(symb["symbol"]) for _, symb in act_symbols.items()])
216272

217273
expected_parameter_symbols = free_symbols - activity_symbols
218274

219-
known_parameters = ImpactModelParams.from_list(parameters_registry.values())
220-
221275
forbidden_parameter_names = list(
222276
itertools.chain(
223277
*[
224278
[
225279
elem.name
226-
for elem in known_parameters.find_corresponding_parameter(
280+
for elem in self.parameters.find_corresponding_parameter(
227281
activity_symbol, must_find_one=False
228282
)
229283
]
@@ -234,43 +288,47 @@ def build_impact_tree_and_parameters(
234288

235289
try:
236290
if len(forbidden_parameter_names) > 0:
237-
raise ValueError(
291+
raise ParameterError(
238292
f"Parameter names {forbidden_parameter_names} are forbidden as they "
239293
f"correspond to background activities."
240294
)
241-
except ValueError:
242-
logger.exception("ValueError")
243-
raise
244-
245-
used_parameters = [
246-
known_parameters.find_corresponding_parameter(expected_parameter_symbol)
247-
for expected_parameter_symbol in expected_parameter_symbols
248-
]
249-
unique_used_parameters = []
250-
[
251-
unique_used_parameters.append(i)
252-
for i in used_parameters
253-
if i not in unique_used_parameters
254-
]
255-
unique_used_parameters = ImpactModelParams.from_list(unique_used_parameters)
295+
except ParameterError as e:
296+
logger.exception(e)
297+
raise ParameterError(e)
298+
for expected_parameter_symbol in expected_parameter_symbols:
299+
try:
300+
self.parameters.find_corresponding_parameter(expected_parameter_symbol)
301+
except ValueError:
302+
e = (
303+
f"ValueError : {expected_parameter_symbol} is required in the impact"
304+
f" model but is unknown in the config. Please check in the LCA "
305+
f"config."
306+
)
307+
logger.error(e)
308+
raise ParameterError(e)
256309

257310
# Declare used parameters in conf file as a lca_algebraic parameter to enable
258311
# model building (will not be used afterwards)
259-
for parameter in unique_used_parameters:
312+
313+
for parameter in self.parameters:
314+
if parameter.name in _param_registry().keys():
315+
e = f"Parameter {parameter.name} already in lcaa registry."
316+
logging.error(e)
317+
raise ParameterError(e)
260318
if isinstance(parameter, FloatParam):
261319
newFloatParam(
262320
name=parameter.name,
263321
default=parameter.default,
264322
save=False,
265-
dbname="",
323+
dbname=self.user_database_name,
266324
min=0.0,
267325
)
268326
if isinstance(parameter, EnumParam):
269327
newEnumParam(
270328
name=parameter.name,
271329
values=parameter.weights,
272330
default=parameter.default,
273-
dbname="",
331+
dbname=self.user_database_name,
274332
)
275333

276334
# Create dummy reference to biosphere
@@ -299,7 +357,7 @@ def build_impact_tree_and_parameters(
299357
}
300358
)
301359
node.direct_impacts[method] = model_expr.xreplace(sub)
302-
return tree, unique_used_parameters
360+
return tree, self.parameters
303361

304362
@staticmethod
305363
@with_db_context

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ dependencies = [
4242
"kaleido",
4343
"tqdm",
4444
"ruamel.yaml",
45-
"apparun==0.3.6",
45+
"apparun==0.3.7",
4646
"typer==0.15.1",
4747
"ipython>=7.6.0,<=8.34.0",
4848
"mermaid-py==0.7.1"

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ aenum
2020
kaleido
2121
tqdm
2222
ruamel.yaml
23-
apparun==0.3.6
23+
apparun==0.3.7
2424
ipython>=7.6.0,<=8.34.0
2525
pre-commit
2626
hatchling

samples/conf/nvidia_ai_gpu_chip_lca_conf.yaml

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,17 @@ model:
2020
report:
2121
link: https://appalca.github.io/
2222
description: "A mock example of Appa LCA's impact model corresponding to a fictive AI chip accelerator based on NVIDIA GPU."
23-
date: 03/11/2023
23+
date: 07/10/2025
2424
version: "1"
2525
license: proprietary
26-
appabuild_version: "0.2"
26+
appabuild_version: "0.3.6"
2727
parameters:
2828
- name: cuda_core
2929
type: float
30-
default: 512
30+
default:
31+
architecture:
32+
Maxwell: 1344
33+
Pascal: 1280
3134
min: 256
3235
max: 4096
3336
- name: architecture
@@ -44,14 +47,17 @@ model:
4447
EU: 1
4548
- name: energy_per_inference
4649
type: float
47-
default: 110.0
50+
default:
51+
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.
52+
Maxwell: "0.0878*cuda_core*1000/(90*3600)"
53+
Pascal: "0.0679*cuda_core*1000/(90*3600)"
4854
pm_perc: 0.2
4955
- name: lifespan
5056
type: float
5157
default: 2.0
5258
pm: 1
5359
- name: inference_per_day
5460
type: float
55-
default: 86400000
61+
default: "30*3600*8" #30fps 8 hours a day
5662
min: 0
5763
max: 86400000

0 commit comments

Comments
 (0)