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 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]
@@ -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