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
2 changes: 1 addition & 1 deletion appabuild/cli/lca.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def build(
except (ValueError, ValidationError, BwDatabaseError):
sys.exit(1)
except Exception as e:
logger.error(str(e))
logger.exception(str(e))
sys.exit(1)


Expand Down
109 changes: 83 additions & 26 deletions appabuild/config/lca.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,18 @@

from __future__ import annotations

from typing import Annotated, Any, List, Optional
from typing import List, Optional

import yaml
from apparun.expressions import ParamsValuesSet
from apparun.impact_model import ModelMetadata
from apparun.parameters import ImpactModelParam
from pydantic import BaseModel, BeforeValidator, ValidationError
from apparun.parameters import ImpactModelParam, ImpactModelParams
from pydantic import BaseModel, ValidationError, field_validator
from pydantic_core import PydanticCustomError

from appabuild.logger import log_validation_error, logger


def parse_param(param: dict) -> ImpactModelParam:
"""
Create an ImpactModelParam from a dict.
This dict must contain the key type, if not a PydanticCustomError of type key_error is raised.

:param param: dict containing the elements needed to build an ImpactModelParam.
:return: constructed ImpactModelParam.
"""
if "default" not in param:
raise PydanticCustomError(
"key_error", "Missing field type for a parameter", {"field": "default"}
)
try:
return ImpactModelParam.from_dict(param)
except KeyError:
raise PydanticCustomError(
"key_error", "Missing field type for a parameter", {"field": "type"}
)


class Model(BaseModel):
"""
Contain information about an impact model, like its metadata, the name of the output file
Expand All @@ -52,9 +33,85 @@ class Model(BaseModel):
path: Optional[str] = "."
compile: bool
metadata: Optional[ModelMetadata] = None
parameters: Optional[
List[Annotated[ImpactModelParam, BeforeValidator(parse_param)]]
] = []
parameters: Optional[List[ImpactModelParam]] = []

@field_validator("parameters", mode="before")
@classmethod
def parse_parameters(cls, parameters: List[dict]) -> List[ImpactModelParam]:
parsed_parameters = []

if parameters:
errors = []
for idx, parameter in enumerate(parameters):
if "type" not in parameter:
errors.append(
{
"loc": (idx, "type"),
"msg": "",
"type": PydanticCustomError(
"missing",
"Missing field {field} for a parameter",
{"field": "type"},
),
"input": parameter,
}
)
continue
elif "default" not in parameter:
errors.append(
{
"loc": (idx, "default"),
"msg": "",
"type": PydanticCustomError(
"missing",
"Missing field {field} for a parameter",
{"field": "default"},
),
"input": parameter,
}
)
continue

if (
parameter["type"] == "enum"
and isinstance(parameter["default"], str)
or parameter["type"] == "float"
and isinstance(parameter["default"], (float, int))
):
default = parameter["default"]
else:
default = parameter["default"]
parameter.pop("default")

try:
parsed_param = ImpactModelParam.from_dict(parameter)
parsed_param.default = default
parsed_parameters.append(parsed_param)
except ValidationError as e:
for err in e.errors():
loc = list(err["loc"])
loc.insert(-2, idx)
err["loc"] = tuple(loc)
errors.append(err)

if errors:
raise ValidationError.from_exception_data("", line_errors=errors)

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
2 changes: 2 additions & 0 deletions appabuild/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,5 @@ def log_validation_error(e: ValidationError):
error["ctx"]["name"],
loc_to_str(error["loc"]),
)
case _:
logger.error(error["msg"])
2 changes: 1 addition & 1 deletion appabuild/model/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
)
from lca_algebraic.params import _fixed_params, newEnumParam, newFloatParam
from pydantic import ValidationError
from sympy import Expr, simplify, symbols
from sympy import Expr, simplify, symbols, sympify

from appabuild.config.lca import LCAConfig
from appabuild.database.databases import ForegroundDatabase, parameters_registry
Expand Down
2 changes: 1 addition & 1 deletion samples/conf/appalca_conf.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ project_name: 'sample_project'
databases:
foreground:
name: user_database
path: './samples/datasets/user_database/'
path: './samples/datasets/user_database/nvidia_ai_gpu'
25 changes: 13 additions & 12 deletions samples/conf/nvidia_ai_gpu_chip_lca_conf.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
scope:
fu:
name: 'nvidia_ai_gpu_chip'
database: "test"
database: "user_database"
methods:
- "EFV3_CLIMATE_CHANGE"
model:
Expand All @@ -28,7 +28,8 @@ model:
- name: cuda_core
type: float
default: 512
pm_perc: 0.1
min: 256
max: 4096
- name: architecture
type: enum
default: Maxwell
Expand All @@ -37,20 +38,20 @@ model:
Pascal: 1
- name: usage_location
type: enum
default: EU
default: FR
weights:
FR: 1
EU: 1
- name: energy_per_inference
type: float
default: 0.05
min: 0.01
max: 0.1
- name: inference_per_day
type: float
default: 3600
pm_perc: 0.1
default: 110.0
pm_perc: 0.2
- name: lifespan
type: float
default: 2
pm_perc: 0.1
default: 2.0
pm: 1
- name: inference_per_day
type: float
default: 86400000
min: 0
max: 86400000
6 changes: 3 additions & 3 deletions tests/data/cmd_build/nvidia_ai_gpu_chip_expected.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@ parameters:
pm: null
pm_perc: 0.1
- name: lifespan
default: 2.0
default: 3.6
type: float
min: 1.8
max: 2.2
min: 3.24
max: 3.96
distrib: linear
pm: null
pm_perc: 0.1
Expand Down
7 changes: 5 additions & 2 deletions tests/data/cmd_build/nvidia_ai_gpu_chip_lca_conf.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ model:
parameters:
- name: cuda_core
type: float
default: 512
default:
architecture:
Maxwell: 512
Pascal: 560
pm_perc: 0.1
- name: architecture
type: enum
Expand All @@ -52,5 +55,5 @@ model:
pm_perc: 0.1
- name: lifespan
type: float
default: 2
default: inference_per_day / 1000
pm_perc: 0.1
7 changes: 5 additions & 2 deletions tests/data/lca_confs/valids/nvidia_ai_gpu_chip_lca_conf.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ model:
parameters:
- name: cuda_core
type: float
default: 512
default:
architecture:
Maxwell: 550
Pascal: 600
pm_perc: 0.1
- name: architecture
type: enum
Expand All @@ -52,5 +55,5 @@ model:
pm_perc: 0.1
- name: lifespan
type: float
default: 2
default: log(inference_per_day)
pm_perc: 0.1
6 changes: 3 additions & 3 deletions tests/end_to_end/test_cmd_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ def test_build_command():

# Check the generated impact model is the same as expected
with open(expected_file, "r") as stream:
f1_yaml = yaml.safe_load(stream)
expected = yaml.safe_load(stream)

with open("nvidia_ai_gpu_chip.yaml", "r") as stream:
f2_yaml = yaml.safe_load(stream)
value = yaml.safe_load(stream)

os.remove("nvidia_ai_gpu_chip.yaml")

assert f1_yaml == f2_yaml, "result file not the same as expected file "
assert expected == value, "result file not the same as expected file "
6 changes: 1 addition & 5 deletions tests/functional_no_db/test_lca_conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,10 @@ def test_missing_required_fields():
pytest.fail("An LCA config with missing fields is not a valid config")
except ValidationError as e:
for error in e.errors():
assert error["type"] == "missing" or error["type"] == "key_error"
assert error["type"] == "missing"
if error["type"] == "missing":
assert error["loc"] in missing_fields_locs
missing_fields_locs.remove(error["loc"])
else:
loc = error["loc"] + (error["ctx"]["field"],)
assert loc in missing_fields_locs
missing_fields_locs.remove(loc)
assert len(missing_fields_locs) == 0


Expand Down