diff --git a/appabuild/cli/lca.py b/appabuild/cli/lca.py index 1cd39f4..cf62506 100644 --- a/appabuild/cli/lca.py +++ b/appabuild/cli/lca.py @@ -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) diff --git a/appabuild/config/lca.py b/appabuild/config/lca.py index 3c9a0d3..d940aae 100644 --- a/appabuild/config/lca.py +++ b/appabuild/config/lca.py @@ -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 @@ -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)) diff --git a/appabuild/logger.py b/appabuild/logger.py index 4560152..ddcf146 100644 --- a/appabuild/logger.py +++ b/appabuild/logger.py @@ -79,3 +79,5 @@ def log_validation_error(e: ValidationError): error["ctx"]["name"], loc_to_str(error["loc"]), ) + case _: + logger.error(error["msg"]) diff --git a/appabuild/model/builder.py b/appabuild/model/builder.py index d8e93ae..33ddfb8 100644 --- a/appabuild/model/builder.py +++ b/appabuild/model/builder.py @@ -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 diff --git a/samples/conf/appalca_conf.yaml b/samples/conf/appalca_conf.yaml index f42416a..9bd3939 100644 --- a/samples/conf/appalca_conf.yaml +++ b/samples/conf/appalca_conf.yaml @@ -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' diff --git a/samples/conf/nvidia_ai_gpu_chip_lca_conf.yaml b/samples/conf/nvidia_ai_gpu_chip_lca_conf.yaml index 9e481a8..0b32752 100644 --- a/samples/conf/nvidia_ai_gpu_chip_lca_conf.yaml +++ b/samples/conf/nvidia_ai_gpu_chip_lca_conf.yaml @@ -1,7 +1,7 @@ scope: fu: name: 'nvidia_ai_gpu_chip' - database: "test" + database: "user_database" methods: - "EFV3_CLIMATE_CHANGE" model: @@ -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 @@ -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 \ No newline at end of file + default: 2.0 + pm: 1 + - name: inference_per_day + type: float + default: 86400000 + min: 0 + max: 86400000 \ No newline at end of file diff --git a/tests/data/cmd_build/nvidia_ai_gpu_chip_expected.yaml b/tests/data/cmd_build/nvidia_ai_gpu_chip_expected.yaml index d24e5d5..ea33b4d 100644 --- a/tests/data/cmd_build/nvidia_ai_gpu_chip_expected.yaml +++ b/tests/data/cmd_build/nvidia_ai_gpu_chip_expected.yaml @@ -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 diff --git a/tests/data/cmd_build/nvidia_ai_gpu_chip_lca_conf.yaml b/tests/data/cmd_build/nvidia_ai_gpu_chip_lca_conf.yaml index e1dc1dd..06e20f8 100644 --- a/tests/data/cmd_build/nvidia_ai_gpu_chip_lca_conf.yaml +++ b/tests/data/cmd_build/nvidia_ai_gpu_chip_lca_conf.yaml @@ -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 @@ -52,5 +55,5 @@ model: pm_perc: 0.1 - name: lifespan type: float - default: 2 + default: inference_per_day / 1000 pm_perc: 0.1 \ No newline at end of file diff --git a/tests/data/lca_confs/valids/nvidia_ai_gpu_chip_lca_conf.yaml b/tests/data/lca_confs/valids/nvidia_ai_gpu_chip_lca_conf.yaml index a4193a7..348846d 100644 --- a/tests/data/lca_confs/valids/nvidia_ai_gpu_chip_lca_conf.yaml +++ b/tests/data/lca_confs/valids/nvidia_ai_gpu_chip_lca_conf.yaml @@ -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 @@ -52,5 +55,5 @@ model: pm_perc: 0.1 - name: lifespan type: float - default: 2 + default: log(inference_per_day) pm_perc: 0.1 diff --git a/tests/end_to_end/test_cmd_build.py b/tests/end_to_end/test_cmd_build.py index 6ae5622..30a532e 100644 --- a/tests/end_to_end/test_cmd_build.py +++ b/tests/end_to_end/test_cmd_build.py @@ -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 " diff --git a/tests/functional_no_db/test_lca_conf.py b/tests/functional_no_db/test_lca_conf.py index 9209064..2ed2338 100644 --- a/tests/functional_no_db/test_lca_conf.py +++ b/tests/functional_no_db/test_lca_conf.py @@ -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