apparun 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- app/__init__.py +0 -0
- app/api/app.py +43 -0
- app/cli/__init__.py +0 -0
- app/cli/main.py +62 -0
- apparun/__init__.py +0 -0
- apparun/core.py +118 -0
- apparun/impact_methods.py +129 -0
- apparun/impact_model.py +318 -0
- apparun/impact_tree.py +249 -0
- apparun/parameters.py +340 -0
- apparun/results.py +381 -0
- apparun/score.py +51 -0
- apparun/tree_node.py +97 -0
- apparun-0.3.0.dist-info/METADATA +317 -0
- apparun-0.3.0.dist-info/RECORD +18 -0
- apparun-0.3.0.dist-info/WHEEL +4 -0
- apparun-0.3.0.dist-info/entry_points.txt +2 -0
- apparun-0.3.0.dist-info/licenses/LICENSE.md +277 -0
app/__init__.py
ADDED
|
File without changes
|
app/api/app.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from typing import Dict, List, Union
|
|
2
|
+
|
|
3
|
+
from fastapi import FastAPI
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
import apparun.core
|
|
7
|
+
|
|
8
|
+
app = FastAPI()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ComputeParams(BaseModel):
|
|
12
|
+
impact_model_name: str
|
|
13
|
+
params: Dict[str, Union[str, float, List[Union[str, float]]]]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GetModelParams(BaseModel):
|
|
17
|
+
impact_model_name: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@app.post("/compute/")
|
|
21
|
+
def compute(params: ComputeParams):
|
|
22
|
+
scores = apparun.core.compute_impacts(params.impact_model_name, params.params)
|
|
23
|
+
return scores
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@app.post("/compute_nodes/")
|
|
27
|
+
def compute_nodes(params: ComputeParams):
|
|
28
|
+
scores = apparun.core.compute_impacts(
|
|
29
|
+
params.impact_model_name, params.params, all_nodes=True
|
|
30
|
+
)
|
|
31
|
+
return scores
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@app.get("/get_models/")
|
|
35
|
+
def get_models():
|
|
36
|
+
valid_impact_models = apparun.core.get_valid_models()
|
|
37
|
+
return valid_impact_models
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@app.post("/get_model_params/")
|
|
41
|
+
def get_model_params(params: GetModelParams):
|
|
42
|
+
impact_models_params = apparun.core.get_model_params(params.impact_model_name)
|
|
43
|
+
return impact_models_params
|
app/cli/__init__.py
ADDED
|
File without changes
|
app/cli/main.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
import yaml
|
|
6
|
+
|
|
7
|
+
import apparun.core
|
|
8
|
+
|
|
9
|
+
cli_app = typer.Typer()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@cli_app.command()
|
|
13
|
+
def compute(
|
|
14
|
+
impact_model_name: str,
|
|
15
|
+
params_file_path: str,
|
|
16
|
+
output_file_path: Optional[str] = None,
|
|
17
|
+
):
|
|
18
|
+
with open(params_file_path, "r") as stream:
|
|
19
|
+
params = yaml.safe_load(stream) or {}
|
|
20
|
+
scores = apparun.core.compute_impacts(impact_model_name, params)
|
|
21
|
+
print(scores)
|
|
22
|
+
if output_file_path is not None:
|
|
23
|
+
with open(output_file_path, "w") as stream:
|
|
24
|
+
yaml.dump(scores, stream, sort_keys=False)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@cli_app.command()
|
|
28
|
+
def compute_nodes(
|
|
29
|
+
impact_model_name: str,
|
|
30
|
+
params_file_path: str,
|
|
31
|
+
output_file_path: Optional[str] = None,
|
|
32
|
+
):
|
|
33
|
+
with open(params_file_path, "r") as stream:
|
|
34
|
+
params = yaml.safe_load(stream) or {}
|
|
35
|
+
scores = apparun.core.compute_impacts(impact_model_name, params, all_nodes=True)
|
|
36
|
+
print(scores)
|
|
37
|
+
if output_file_path is not None:
|
|
38
|
+
with open(output_file_path, "w") as stream:
|
|
39
|
+
yaml.dump(scores, stream, sort_keys=False)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@cli_app.command()
|
|
43
|
+
def models():
|
|
44
|
+
valid_impact_models = apparun.core.get_valid_models()
|
|
45
|
+
print(valid_impact_models)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@cli_app.command()
|
|
49
|
+
def model_params(impact_model_name: str):
|
|
50
|
+
impact_model_params = apparun.core.get_model_params(impact_model_name)
|
|
51
|
+
print(impact_model_params)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@cli_app.command()
|
|
55
|
+
def results(results_config_file_path: str):
|
|
56
|
+
with open(results_config_file_path, "r") as stream:
|
|
57
|
+
results_config = yaml.safe_load(stream)
|
|
58
|
+
apparun.core.compute_results(results_config)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if __name__ == "__main__":
|
|
62
|
+
cli_app()
|
apparun/__init__.py
ADDED
|
File without changes
|
apparun/core.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
from functools import wraps
|
|
5
|
+
from typing import Dict, List, Union
|
|
6
|
+
|
|
7
|
+
from apparun import results
|
|
8
|
+
from apparun.impact_model import ImpactModel
|
|
9
|
+
from apparun.results import get_result
|
|
10
|
+
|
|
11
|
+
IMPACTS_MODEL_DIR = os.environ.get("APPARUN_IMPACT_MODELS_DIR")
|
|
12
|
+
logging.basicConfig(
|
|
13
|
+
filename="apparun.log",
|
|
14
|
+
filemode="w",
|
|
15
|
+
format="%(name)s - %(levelname)s - %(message)s",
|
|
16
|
+
level=logging.INFO,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def execution_time_logging(func):
|
|
21
|
+
"""
|
|
22
|
+
Decorator to log execution time as an info.
|
|
23
|
+
:param func: function to wrap.
|
|
24
|
+
:return: wrapped function.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
@wraps(func)
|
|
28
|
+
def execution_time_logging_wrapper(*args, **kwargs):
|
|
29
|
+
start_time = time.perf_counter()
|
|
30
|
+
result = func(*args, **kwargs)
|
|
31
|
+
end_time = time.perf_counter()
|
|
32
|
+
total_time = end_time - start_time
|
|
33
|
+
logging.info(f"Function {func.__name__} executed in {total_time:.4f} seconds")
|
|
34
|
+
return result
|
|
35
|
+
|
|
36
|
+
return execution_time_logging_wrapper
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@execution_time_logging
|
|
40
|
+
def compute_impacts(
|
|
41
|
+
impact_model_name: str, params: Dict, all_nodes: bool = False
|
|
42
|
+
) -> Union[List, Dict[str, Union[float, List[float]]]]:
|
|
43
|
+
"""
|
|
44
|
+
Load an impact model from disk from its name, and get impact scores of the root node
|
|
45
|
+
for each impact method, according to the parameters.
|
|
46
|
+
APPARUN_IMPACT_MODELS_DIR environment variable should be specified (see README.md).
|
|
47
|
+
:param impact_model_name: name of the impact model to load
|
|
48
|
+
:param params: value, or list of values of the impact model's parameters.
|
|
49
|
+
List of values must have the same length. If single values are provided
|
|
50
|
+
alongside a list of values, it will be duplicated to the appropriate length.
|
|
51
|
+
:param all_nodes: if True, scores will be computed for each node. Only root node
|
|
52
|
+
otherwise (default).
|
|
53
|
+
:return: a dict mapping impact names and corresponding score, or list of scores.
|
|
54
|
+
"""
|
|
55
|
+
impact_model = ImpactModel.from_yaml(
|
|
56
|
+
os.path.join(IMPACTS_MODEL_DIR, f"{impact_model_name}.yaml")
|
|
57
|
+
)
|
|
58
|
+
if all_nodes:
|
|
59
|
+
return impact_model.get_nodes_scores(**params)
|
|
60
|
+
return dict(impact_model.get_scores(**params))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@execution_time_logging
|
|
64
|
+
def get_valid_models() -> List[str]:
|
|
65
|
+
"""
|
|
66
|
+
Get a list of all valid impact models in the directory specified by
|
|
67
|
+
APPARUN_IMPACT_MODELS_DIR environment variable.
|
|
68
|
+
:return: a list of all valid impact models.
|
|
69
|
+
"""
|
|
70
|
+
valid_impact_models = []
|
|
71
|
+
for file in os.listdir(IMPACTS_MODEL_DIR):
|
|
72
|
+
if file.endswith(".yaml"):
|
|
73
|
+
try:
|
|
74
|
+
ImpactModel.from_yaml(os.path.join(IMPACTS_MODEL_DIR, file))
|
|
75
|
+
valid_impact_models.append(file.replace(".yaml", ""))
|
|
76
|
+
except KeyError:
|
|
77
|
+
print(f"{file.replace('.yaml', '')} is not a valid impact model.")
|
|
78
|
+
return valid_impact_models
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@execution_time_logging
|
|
82
|
+
def get_model_params(impact_model_name: str) -> List[Dict]:
|
|
83
|
+
"""
|
|
84
|
+
Return the list of parameters required to execute an impact model.
|
|
85
|
+
APPARUN_IMPACT_MODELS_DIR environment variable should be specified (see README.md).
|
|
86
|
+
:param impact_model_name: name of the impact model to load.
|
|
87
|
+
:return: a list of parameters required by the model.
|
|
88
|
+
"""
|
|
89
|
+
impact_model = ImpactModel.from_yaml(
|
|
90
|
+
os.path.join(IMPACTS_MODEL_DIR, f"{impact_model_name}.yaml")
|
|
91
|
+
)
|
|
92
|
+
return [parameter.to_dict() for parameter in impact_model.parameters]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@execution_time_logging
|
|
96
|
+
def compute_results(results_config: List[Dict]):
|
|
97
|
+
"""
|
|
98
|
+
Generate results according to results_config.
|
|
99
|
+
:param results_config: list of results wanted. Each element of this list will be
|
|
100
|
+
used to construct an ImpactModelResult, using the elements in "args" argument.
|
|
101
|
+
Result subclass is determined by "class" argument.
|
|
102
|
+
"""
|
|
103
|
+
for result_config in results_config:
|
|
104
|
+
result_name = result_config["result_name"]
|
|
105
|
+
result_constructor_args = result_config["args"]
|
|
106
|
+
if "parameters" in result_constructor_args["impact_model"]:
|
|
107
|
+
result_constructor_args["parameters"] = result_constructor_args[
|
|
108
|
+
"impact_model"
|
|
109
|
+
]["parameters"]
|
|
110
|
+
result_constructor_args["impact_model"] = ImpactModel.from_yaml(
|
|
111
|
+
os.path.join(
|
|
112
|
+
IMPACTS_MODEL_DIR,
|
|
113
|
+
f"{result_constructor_args['impact_model']['name']}.yaml",
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
result_class = get_result(result_name)
|
|
117
|
+
result_objet = result_class(**result_constructor_args)
|
|
118
|
+
result_objet.run()
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from aenum import Enum, NoAlias
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MethodFullName(str, Enum):
|
|
7
|
+
"""
|
|
8
|
+
Enumeration of impact methods supported by Brightway.
|
|
9
|
+
So far, only some ReCiPe methods, and all EF v3.0 impact methods are supported.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
# EFV3
|
|
13
|
+
EFV3_ACIDIFICATION = "('EF v3.0', 'acidification', 'accumulated exceedance (AE)')"
|
|
14
|
+
EFV3_CLIMATE_CHANGE = (
|
|
15
|
+
"('EF v3.0', 'climate change', 'global warming potential (GWP100)')"
|
|
16
|
+
)
|
|
17
|
+
EFV3_CLIMATE_CHANGE_BIOGENIC = (
|
|
18
|
+
"('EF v3.0', 'climate change: biogenic', 'global warming potential (GWP100)')"
|
|
19
|
+
)
|
|
20
|
+
EFV3_CLIMATE_CHANGE_FOSSIL = (
|
|
21
|
+
"('EF v3.0', 'climate change: fossil', 'global warming potential (GWP100)')"
|
|
22
|
+
)
|
|
23
|
+
EFV3_CLIMATE_CHANGE_LAND_USE = "('EF v3.0', 'climate change: land use and land use change', 'global warming potential (GWP100)')"
|
|
24
|
+
EFV3_ECOTOXICITY_FRESHWATER = "('EF v3.0', 'ecotoxicity: freshwater', 'comparative toxic unit for ecosystems (CTUe)')"
|
|
25
|
+
EFV3_ECOTOXICITY_FRESHWATER_INORGANICS = "('EF v3.0', 'ecotoxicity: freshwater, inorganics', 'comparative toxic unit for ecosystems (CTUe)')"
|
|
26
|
+
EFV3_ECOTOXICITY_FRESHWATER_METALS = "('EF v3.0', 'ecotoxicity: freshwater, metals', 'comparative toxic unit for ecosystems (CTUe)')"
|
|
27
|
+
EFV3_ECOTOXICITY_FRESHWATER_ORGANICS = "('EF v3.0', 'ecotoxicity: freshwater, organics', 'comparative toxic unit for ecosystems (CTUe)')"
|
|
28
|
+
EFV3_EUTROPHICATION_FRESHWATER = "('EF v3.0', 'eutrophication: freshwater', 'fraction of nutrients reaching freshwater end compartment (P)')"
|
|
29
|
+
EFV3_EUTROPHICATION_MARINE = "('EF v3.0', 'eutrophication: marine', 'fraction of nutrients reaching marine end compartment (N)')"
|
|
30
|
+
EFV3_EUTROPHICATION_TERRESTRIAL = (
|
|
31
|
+
"('EF v3.0', 'eutrophication: terrestrial', 'accumulated exceedance (AE)')"
|
|
32
|
+
)
|
|
33
|
+
EFV3_HUMAN_TOXICITY_CARCINOGENIC = "('EF v3.0', 'human toxicity: carcinogenic', 'comparative toxic unit for human (CTUh)')"
|
|
34
|
+
EFV3_HUMAN_TOXICITY_CARCINOGENIC_INORGANICS = "('EF v3.0', 'human toxicity: carcinogenic, inorganics', 'comparative toxic unit for human (CTUh)')"
|
|
35
|
+
EFV3_HUMAN_TOXICITY_CARCINOGENIC_METALS = "('EF v3.0', 'human toxicity: carcinogenic, metals', 'comparative toxic unit for human (CTUh)')"
|
|
36
|
+
EFV3_HUMAN_TOXICITY_CARCINOGENIC_ORGANICS = "('EF v3.0', 'human toxicity: carcinogenic, organics', 'comparative toxic unit for human (CTUh)')"
|
|
37
|
+
EFV3_HUMAN_TOXICITY_NON_CARCINOGENIC = "('EF v3.0', 'human toxicity: non-carcinogenic', 'comparative toxic unit for human (CTUh)')"
|
|
38
|
+
EFV3_HUMAN_TOXICITY_NON_CARCINOGENIC_INORGANICS = "('EF v3.0', 'human toxicity: non-carcinogenic, inorganics', 'comparative toxic unit for human (CTUh)')"
|
|
39
|
+
EFV3_HUMAN_TOXICITY_NON_CARCINOGENIC_METALS = "('EF v3.0', 'human toxicity: non-carcinogenic, metals', 'comparative toxic unit for human (CTUh)')"
|
|
40
|
+
EFV3_HUMAN_TOXICITY_NON_CARCINOGENIC_ORGANICS = "('EF v3.0', 'human toxicity: non-carcinogenic, organics', 'comparative toxic unit for human (CTUh)')"
|
|
41
|
+
EFV3_IONISING_RADIATION = "('EF v3.0', 'ionising radiation: human health', 'human exposure efficiency relative to u235')"
|
|
42
|
+
EFV3_LAND_USE = "('EF v3.0', 'land use', 'soil quality index')"
|
|
43
|
+
EFV3_MATERIAL_RESOURCES = "('EF v3.0', 'material resources: metals/minerals', 'abiotic depletion potential (ADP): elements (ultimate reserves)')"
|
|
44
|
+
EFV3_ENERGY_RESOURCES = "('EF v3.0', 'energy resources: non-renewable', 'abiotic depletion potential (ADP): fossil fuels')"
|
|
45
|
+
EFV3_OZONE_DEPLETION = (
|
|
46
|
+
"('EF v3.0', 'ozone depletion', 'ozone depletion potential (ODP)')"
|
|
47
|
+
)
|
|
48
|
+
EFV3_PARTICULATE_MATTER_FORMATION = (
|
|
49
|
+
"('EF v3.0', 'particulate matter formation', 'impact on human health')"
|
|
50
|
+
)
|
|
51
|
+
EFV3_PHOTOCHEMICAL_OZONE_FORMATION = "('EF v3.0', 'photochemical oxidant formation: human health', 'tropospheric ozone concentration increase')"
|
|
52
|
+
EFV3_WATER_USE = "('EF v3.0', 'water use', 'user deprivation potential (deprivation-weighted water consumption)')"
|
|
53
|
+
|
|
54
|
+
def to_short_name(self) -> MethodShortName:
|
|
55
|
+
"""
|
|
56
|
+
Convert a full impact name (as specified in Brightway) to its shorter version.
|
|
57
|
+
:return: short name version of full impact name.
|
|
58
|
+
"""
|
|
59
|
+
return MethodShortName[self.name]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class MethodShortName(str, Enum):
|
|
63
|
+
"""
|
|
64
|
+
Short version of impact methods supported by Brightway, to ease readability of
|
|
65
|
+
figures.
|
|
66
|
+
So far, only some ReCiPe methods, and all EF v3.0 impact methods are supported.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
_settings_ = NoAlias
|
|
70
|
+
# RECIPE
|
|
71
|
+
RECIPE_MIDPOINT_CLIMATE_CHANGE = "Climate change (GWP100)"
|
|
72
|
+
RECIPE_MIDPOINT_TERRESTRIAL_ECOTOXICITY = "Terrestrial ecotoxicity (TETPinf)"
|
|
73
|
+
RECIPE_ENDPOINT_ECOSYSTEM_QUALITY = "Damage to ecosystems"
|
|
74
|
+
RECIPE_ENDPOINT_RESOURCES = "Damage to resource availability"
|
|
75
|
+
RECIPE_ENDPOINT_HUMAN_HEALTH = "Damage to human health"
|
|
76
|
+
RECIPE_ENDPOINT_TOTAL = "Total damage"
|
|
77
|
+
|
|
78
|
+
# EFV3
|
|
79
|
+
EFV3_ACIDIFICATION = "Acification (AE)"
|
|
80
|
+
EFV3_CLIMATE_CHANGE = "Climate change (GWP100)"
|
|
81
|
+
EFV3_CLIMATE_CHANGE_BIOGENIC = "Climate change: biogenic (GWP100)"
|
|
82
|
+
EFV3_CLIMATE_CHANGE_FOSSIL = "Climate change: fossil (GWP100)"
|
|
83
|
+
EFV3_CLIMATE_CHANGE_LAND_USE = "Climate change: land use/land use change (GWP100)"
|
|
84
|
+
EFV3_ECOTOXICITY_FRESHWATER = "Ecotoxicity: freshwater (CTUe)"
|
|
85
|
+
EFV3_ECOTOXICITY_FRESHWATER_INORGANICS = (
|
|
86
|
+
"Ecotoxicity: freshwater, inorganics (CTUe)"
|
|
87
|
+
)
|
|
88
|
+
EFV3_ECOTOXICITY_FRESHWATER_METALS = "Ecotoxicity: freshwater, metals (CTUe)"
|
|
89
|
+
EFV3_ECOTOXICITY_FRESHWATER_ORGANICS = "Ecotoxicity: freshwater, organics (CTUe)"
|
|
90
|
+
EFV3_EUTROPHICATION_FRESHWATER = "Eutrophication: freshwater (kgPeq)"
|
|
91
|
+
EFV3_EUTROPHICATION_MARINE = "Eutrophication: marine (N)"
|
|
92
|
+
EFV3_EUTROPHICATION_TERRESTRIAL = "Eutrophication: terrestrial (AE)"
|
|
93
|
+
EFV3_HUMAN_TOXICITY_CARCINOGENIC = "Human toxicity: carcinogenic (CTUh)"
|
|
94
|
+
EFV3_HUMAN_TOXICITY_CARCINOGENIC_INORGANICS = (
|
|
95
|
+
"Human toxicity: carcinogenic, inorganics (CTUh)"
|
|
96
|
+
)
|
|
97
|
+
EFV3_HUMAN_TOXICITY_CARCINOGENIC_METALS = (
|
|
98
|
+
"Human toxicity: carcinogenic, metals (CTUh)"
|
|
99
|
+
)
|
|
100
|
+
EFV3_HUMAN_TOXICITY_CARCINOGENIC_ORGANICS = (
|
|
101
|
+
"Human toxicity: carcinogenic, organics (CTUh)"
|
|
102
|
+
)
|
|
103
|
+
EFV3_HUMAN_TOXICITY_NON_CARCINOGENIC = "Human toxicity: non-carcinogenic (CTUh)"
|
|
104
|
+
EFV3_HUMAN_TOXICITY_NON_CARCINOGENIC_INORGANICS = (
|
|
105
|
+
"Human toxicity: non-carcinogenic, inorganics (CTUh)"
|
|
106
|
+
)
|
|
107
|
+
EFV3_HUMAN_TOXICITY_NON_CARCINOGENIC_METALS = (
|
|
108
|
+
"Human toxicity: non-carcinogenic, metals (CTUh)"
|
|
109
|
+
)
|
|
110
|
+
EFV3_HUMAN_TOXICITY_NON_CARCINOGENIC_ORGANICS = (
|
|
111
|
+
"Human toxicity: non-carcinogenic, organics (CTUh)"
|
|
112
|
+
)
|
|
113
|
+
EFV3_IONISING_RADIATION = "Ionising radiation: human health (kBqU235)"
|
|
114
|
+
EFV3_LAND_USE = "Land use (soil quality index)"
|
|
115
|
+
EFV3_MATERIAL_RESOURCES = "Resource use, metals and minerals (kgSbeq)"
|
|
116
|
+
EFV3_ENERGY_RESOURCES = "Energy use, energy carriers (MJ)"
|
|
117
|
+
EFV3_OZONE_DEPLETION = "Ozone depletion (ODP)"
|
|
118
|
+
EFV3_PARTICULATE_MATTER_FORMATION = (
|
|
119
|
+
"Particulate matter formation: impact on human health (disease incidences)"
|
|
120
|
+
)
|
|
121
|
+
EFV3_PHOTOCHEMICAL_OZONE_FORMATION = "Photochemical ozone formation (kgNMVOCeq)"
|
|
122
|
+
EFV3_WATER_USE = "Depr.-weighted water cons. (kg world eq. deprived)"
|
|
123
|
+
|
|
124
|
+
def to_full_name(self) -> MethodFullName:
|
|
125
|
+
"""
|
|
126
|
+
Convert a short impact name to its full version (as specified in Brightway).
|
|
127
|
+
:return: full name version of short impact name.
|
|
128
|
+
"""
|
|
129
|
+
return MethodFullName[self.name]
|
apparun/impact_model.py
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Callable, Dict, List, Optional, Union
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import yaml
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
from SALib.analyze import sobol
|
|
9
|
+
|
|
10
|
+
from apparun.impact_tree import ImpactTreeNode
|
|
11
|
+
from apparun.parameters import ImpactModelParams
|
|
12
|
+
from apparun.score import LCIAScores
|
|
13
|
+
from apparun.tree_node import NodeScores
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LcaPractitioner(BaseModel):
|
|
17
|
+
"""
|
|
18
|
+
Information about a LCA practitioner.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
name: Optional[str] = None
|
|
22
|
+
organization: Optional[str] = None
|
|
23
|
+
mail: Optional[str] = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class LcaStudy(BaseModel):
|
|
27
|
+
"""
|
|
28
|
+
Information about LCA study, in order to understand its scope and for
|
|
29
|
+
reproducibility.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
link: Optional[str] = None
|
|
33
|
+
description: Optional[str] = None
|
|
34
|
+
date: Optional[str] = None
|
|
35
|
+
version: Optional[str] = None
|
|
36
|
+
license: Optional[str] = None
|
|
37
|
+
appabuild_version: Optional[str] = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ModelMetadata(BaseModel):
|
|
41
|
+
"""
|
|
42
|
+
Contain information various information about the context of production of the
|
|
43
|
+
impact model.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
author: Optional[LcaPractitioner] = None
|
|
47
|
+
reviewer: Optional[LcaPractitioner] = None
|
|
48
|
+
report: Optional[LcaStudy] = None
|
|
49
|
+
|
|
50
|
+
def to_dict(self):
|
|
51
|
+
"""
|
|
52
|
+
Convert self to dict.
|
|
53
|
+
:return: self as a dict
|
|
54
|
+
"""
|
|
55
|
+
return self.model_dump()
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def from_dict(model_metadata: dict) -> ModelMetadata:
|
|
59
|
+
"""
|
|
60
|
+
Convert dict to ModelMetadata object.
|
|
61
|
+
:param model_metadata: dict containing construction parameters of the model
|
|
62
|
+
metadata.
|
|
63
|
+
:return: constructed model metadata.
|
|
64
|
+
"""
|
|
65
|
+
return ModelMetadata(
|
|
66
|
+
author=LcaPractitioner(**model_metadata["author"]),
|
|
67
|
+
reviewer=LcaPractitioner(**model_metadata["reviewer"]),
|
|
68
|
+
report=LcaStudy(**model_metadata["report"]),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class ImpactModel(BaseModel):
|
|
73
|
+
"""
|
|
74
|
+
Impact model contains all the information required to compute the impact of an
|
|
75
|
+
LCA built with Appa Build.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
metadata: Optional[ModelMetadata] = None
|
|
79
|
+
parameters: Optional[ImpactModelParams] = None
|
|
80
|
+
tree: Optional[ImpactTreeNode] = None
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def name(self):
|
|
84
|
+
return self.tree.name
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def transformation_table(
|
|
88
|
+
self,
|
|
89
|
+
) -> Dict[str, Callable[[Union[str, float]], Dict[str, float]]]:
|
|
90
|
+
"""
|
|
91
|
+
Map each parameter to its transform method.
|
|
92
|
+
:return: a dict mapping impact model's parameters' name with their transform
|
|
93
|
+
method.
|
|
94
|
+
"""
|
|
95
|
+
return {parameter.name: parameter.transform for parameter in self.parameters}
|
|
96
|
+
|
|
97
|
+
def transform_parameters(
|
|
98
|
+
self, parameters: Dict[str, Union[List[Union[str, float]], Union[str, float]]]
|
|
99
|
+
) -> Dict[str, Union[List[Union[str, float]], Union[str, float]]]:
|
|
100
|
+
"""
|
|
101
|
+
Transform all the parameters' values, so it can be fed into a node's compute
|
|
102
|
+
method. See ImpactModelParam's transform methods for more information.
|
|
103
|
+
:param parameters: a dict mapping parameters' name and parameters' value, or
|
|
104
|
+
list of values.
|
|
105
|
+
:return: a dict mapping parameters' name and parameters' transformed value, or
|
|
106
|
+
list of transformed values.
|
|
107
|
+
"""
|
|
108
|
+
list_parameters = {
|
|
109
|
+
name: parameter
|
|
110
|
+
for name, parameter in parameters.items()
|
|
111
|
+
if isinstance(parameter, list)
|
|
112
|
+
}
|
|
113
|
+
single_parameters = {
|
|
114
|
+
name: parameter
|
|
115
|
+
for name, parameter in parameters.items()
|
|
116
|
+
if not isinstance(parameter, list)
|
|
117
|
+
}
|
|
118
|
+
if len(list_parameters) == 0:
|
|
119
|
+
return {
|
|
120
|
+
name: value
|
|
121
|
+
for table in [
|
|
122
|
+
self.transformation_table[parameter_name](parameter_value)
|
|
123
|
+
for parameter_name, parameter_value in parameters.items()
|
|
124
|
+
]
|
|
125
|
+
for name, value in table.items()
|
|
126
|
+
}
|
|
127
|
+
assert min(
|
|
128
|
+
len(list_parameter) for list_parameter in list_parameters.values()
|
|
129
|
+
) == max(len(list_parameter) for list_parameter in list_parameters.values())
|
|
130
|
+
full_list_parameters = {
|
|
131
|
+
**{
|
|
132
|
+
parameter_name: [parameter_value]
|
|
133
|
+
* len(list(list_parameters.values())[0])
|
|
134
|
+
for parameter_name, parameter_value in single_parameters.items()
|
|
135
|
+
},
|
|
136
|
+
**list_parameters,
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
name: value
|
|
140
|
+
for table in [
|
|
141
|
+
self.transformation_table[parameter_name](parameter_value)
|
|
142
|
+
for parameter_name, parameter_value in full_list_parameters.items()
|
|
143
|
+
]
|
|
144
|
+
for name, value in table.items()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
def to_dict(self):
|
|
148
|
+
"""
|
|
149
|
+
Convert self to dict.
|
|
150
|
+
:return: self as a dict
|
|
151
|
+
"""
|
|
152
|
+
return {
|
|
153
|
+
"metadata": self.metadata.to_dict(),
|
|
154
|
+
"parameters": self.parameters.to_list(sorted_by_name=True),
|
|
155
|
+
"tree": self.tree.to_dict(),
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
def to_yaml(self, filepath: str, compile_models: bool = True):
|
|
159
|
+
"""
|
|
160
|
+
Convert self to yaml file.
|
|
161
|
+
:param filepath: filepath of the yaml file to create.
|
|
162
|
+
:param compile_models: if True, all models in tree nodes will be compiled.
|
|
163
|
+
ImpactModel will be bigger, but its execution will be faster at first use.
|
|
164
|
+
"""
|
|
165
|
+
if compile_models:
|
|
166
|
+
self.tree.compile_models()
|
|
167
|
+
with open(filepath, "w") as stream:
|
|
168
|
+
yaml.dump(self.to_dict(), stream, sort_keys=False)
|
|
169
|
+
|
|
170
|
+
@staticmethod
|
|
171
|
+
def from_dict(impact_model: dict) -> ImpactModel:
|
|
172
|
+
"""
|
|
173
|
+
Convert dict to ImpactModel object.
|
|
174
|
+
:param impact_model: dict containing construction parameters of the impact
|
|
175
|
+
model.
|
|
176
|
+
:return: constructed impact model.
|
|
177
|
+
"""
|
|
178
|
+
return ImpactModel(
|
|
179
|
+
metadata=ModelMetadata.from_dict(impact_model["metadata"]),
|
|
180
|
+
parameters=ImpactModelParams.from_list(impact_model["parameters"]),
|
|
181
|
+
tree=ImpactTreeNode.from_dict(impact_model["tree"]),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
def from_tree_children(self) -> List[ImpactModel]:
|
|
185
|
+
"""
|
|
186
|
+
Create a new impact model for each of Impact Model tree root node's children.
|
|
187
|
+
Parameters of the impact model are copied, so unused parameters can remain in
|
|
188
|
+
newly created impact models.
|
|
189
|
+
:return: a list of newly created impact models.
|
|
190
|
+
"""
|
|
191
|
+
return [
|
|
192
|
+
ImpactModel(parameters=self.parameters, tree=child)
|
|
193
|
+
for child in self.tree.children
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
@staticmethod
|
|
197
|
+
def from_yaml(filepath: str) -> ImpactModel:
|
|
198
|
+
"""
|
|
199
|
+
Convert a yaml file to an ImpactModel object.
|
|
200
|
+
:param filepath: yaml file containing construction parameters of the impact
|
|
201
|
+
model.
|
|
202
|
+
:return: constructed impact model.
|
|
203
|
+
"""
|
|
204
|
+
with open(filepath, "r") as stream:
|
|
205
|
+
impact_model = yaml.safe_load(stream)
|
|
206
|
+
return ImpactModel.from_dict(impact_model)
|
|
207
|
+
|
|
208
|
+
def get_scores(self, **params) -> LCIAScores:
|
|
209
|
+
"""
|
|
210
|
+
Get impact scores of the root node for each impact method, according to the
|
|
211
|
+
parameters.
|
|
212
|
+
:param params: value, or list of values of the impact model's parameters.
|
|
213
|
+
List of values must have the same length. If single values are provided
|
|
214
|
+
alongside a list of values, it will be duplicated to the appropriate length.
|
|
215
|
+
:return: a dict mapping impact names and corresponding score, or list of scores.
|
|
216
|
+
"""
|
|
217
|
+
missing_params = self.parameters.get_missing_parameter_names(params)
|
|
218
|
+
default_params = self.parameters.get_default_values(missing_params)
|
|
219
|
+
transformed_params = self.transform_parameters({**params, **default_params})
|
|
220
|
+
scores = self.tree.compute(transformed_params)
|
|
221
|
+
return scores
|
|
222
|
+
|
|
223
|
+
def get_nodes_scores(
|
|
224
|
+
self, by_property: Optional[str] = None, **params
|
|
225
|
+
) -> List[NodeScores]:
|
|
226
|
+
"""
|
|
227
|
+
Get impact scores of the each node for each impact method, according to the
|
|
228
|
+
parameters.
|
|
229
|
+
:param by_property: if different than None, results will be pooled by nodes
|
|
230
|
+
sharing the same property value. Property name is the value of by_property.
|
|
231
|
+
:param params: value, or list of values of the impact model's parameters.
|
|
232
|
+
List of values must have the same length. If single values are provided
|
|
233
|
+
alongside a list of values, it will be duplicated to the appropriate length.
|
|
234
|
+
:return: a list of dict mapping impact names and corresponding score, or list
|
|
235
|
+
of scores, for each node/property value.
|
|
236
|
+
"""
|
|
237
|
+
missing_params = self.parameters.get_missing_parameter_names(params)
|
|
238
|
+
default_params = self.parameters.get_default_values(missing_params)
|
|
239
|
+
transformed_params = self.transform_parameters({**params, **default_params})
|
|
240
|
+
scores = [
|
|
241
|
+
NodeScores(
|
|
242
|
+
name=node.name,
|
|
243
|
+
properties=node.properties,
|
|
244
|
+
parent=node.parent.name if node.parent is not None else "",
|
|
245
|
+
lcia_scores=node.compute(
|
|
246
|
+
transformed_params, direct_impacts=by_property is not None
|
|
247
|
+
),
|
|
248
|
+
)
|
|
249
|
+
for node in self.tree.unnested_descendants
|
|
250
|
+
]
|
|
251
|
+
if by_property is not None:
|
|
252
|
+
scores = NodeScores.combine_by_property(scores, by_property)
|
|
253
|
+
return scores
|
|
254
|
+
|
|
255
|
+
def get_uncertainty_nodes_scores(self, n) -> List[NodeScores]:
|
|
256
|
+
""" """
|
|
257
|
+
samples = self.parameters.uniform_draw(n)
|
|
258
|
+
samples = self.parameters.draw_to_distrib(samples)
|
|
259
|
+
nodes_scores = self.get_nodes_scores(**samples)
|
|
260
|
+
return nodes_scores
|
|
261
|
+
|
|
262
|
+
def get_uncertainty_scores(self, n) -> LCIAScores:
|
|
263
|
+
""" """
|
|
264
|
+
samples = self.parameters.uniform_draw(n)
|
|
265
|
+
samples = self.parameters.draw_to_distrib(samples)
|
|
266
|
+
lcia_scores = self.get_scores(**samples)
|
|
267
|
+
return lcia_scores
|
|
268
|
+
|
|
269
|
+
def get_sobol_s1_indices(
|
|
270
|
+
self, n, all_nodes: bool = False
|
|
271
|
+
) -> List[Dict[str, Union[str, np.ndarray]]]:
|
|
272
|
+
"""
|
|
273
|
+
Get sobol first indices, which corresponds to the contribution of each
|
|
274
|
+
parameter to total result variance.
|
|
275
|
+
:param n: number of samples to draw with monte carlo.
|
|
276
|
+
:param all_nodes: if True, sobol s1 indices will be computed for each node. Else,
|
|
277
|
+
only for root node (FU).
|
|
278
|
+
:return: unpivoted dataframe containing sobol first indices for each parameter,
|
|
279
|
+
impact method, and node name if all_nodes is True.
|
|
280
|
+
"""
|
|
281
|
+
samples = self.parameters.sobol_draw(n)
|
|
282
|
+
samples = self.parameters.draw_to_distrib(samples)
|
|
283
|
+
if all_nodes:
|
|
284
|
+
lcia_scores = self.get_nodes_scores(**samples)
|
|
285
|
+
sobol_s1_indices = []
|
|
286
|
+
for node_scores in lcia_scores:
|
|
287
|
+
for method, scores in node_scores.lcia_scores.scores.items():
|
|
288
|
+
s1 = sobol.analyze(
|
|
289
|
+
self.parameters.sobol_problem,
|
|
290
|
+
np.array(scores),
|
|
291
|
+
calc_second_order=True,
|
|
292
|
+
)["S1"]
|
|
293
|
+
sobol_s1_indices += [
|
|
294
|
+
{
|
|
295
|
+
"node": node_scores.name,
|
|
296
|
+
"method": method,
|
|
297
|
+
"parameter": self.parameters.sobol_problem["names"][i],
|
|
298
|
+
"sobol_s1": s1[i],
|
|
299
|
+
}
|
|
300
|
+
for i in range(len(s1))
|
|
301
|
+
]
|
|
302
|
+
return sobol_s1_indices
|
|
303
|
+
lcia_scores = self.get_scores(**samples)
|
|
304
|
+
sobol_s1_indices = []
|
|
305
|
+
for method, scores in lcia_scores.scores.items():
|
|
306
|
+
s1 = sobol.analyze(
|
|
307
|
+
self.parameters.sobol_problem, np.array(scores), calc_second_order=True
|
|
308
|
+
)["S1"]
|
|
309
|
+
sobol_s1_indices += [
|
|
310
|
+
{
|
|
311
|
+
"node": self.tree.name,
|
|
312
|
+
"method": method,
|
|
313
|
+
"parameter": self.parameters.sobol_problem["names"][i],
|
|
314
|
+
"sobol_s1": s1[i],
|
|
315
|
+
}
|
|
316
|
+
for i in range(len(s1))
|
|
317
|
+
]
|
|
318
|
+
return sobol_s1_indices
|