lca-algebraic 1.4.1.dev0__tar.gz → 1.4.1.dev1__tar.gz
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.
- {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/PKG-INFO +2 -1
- {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic/activity.py +10 -8
- {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic/base_utils.py +17 -1
- lca_algebraic-1.4.1.dev1/lca_algebraic/bw_wrapper/__init__.py +150 -0
- lca_algebraic-1.4.1.dev1/lca_algebraic/bw_wrapper/bw2.py +98 -0
- lca_algebraic-1.4.1.dev1/lca_algebraic/bw_wrapper/bw25.py +115 -0
- lca_algebraic-1.4.1.dev1/lca_algebraic/bw_wrapper/protocol.py +81 -0
- {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic/cache.py +15 -5
- {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic/database.py +10 -0
- {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic/lca.py +262 -19
- {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic/log.py +8 -0
- {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic/params.py +1 -3
- {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic/settings.py +7 -0
- {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic.egg-info/PKG-INFO +2 -1
- {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic.egg-info/SOURCES.txt +5 -0
- {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic.egg-info/requires.txt +1 -0
- {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/pyproject.toml +3 -2
- lca_algebraic-1.4.1.dev1/test/test_premise.py +99 -0
- {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/LICENSE +0 -0
- {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/README.md +0 -0
- {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic/__init__.py +0 -0
- {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic/axis_dict.py +0 -0
- {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic/interpolation.py +0 -0
- {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic/io.py +0 -0
- {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic/lambda_expression.py +0 -0
- {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic/matrix.py +0 -0
- {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic/methods.py +0 -0
- {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic/stats.py +0 -0
- {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic/sympy_utils.py +0 -0
- {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic/units.py +0 -0
- {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic.egg-info/dependency_links.txt +0 -0
- {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic.egg-info/top_level.txt +0 -0
- {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/setup.cfg +0 -0
- {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/test/test_axis_dict.py +0 -0
- {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/test/test_axis_matrices_cache.py +0 -0
- {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/test/test_cache.py +0 -0
- {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/test/test_sympy.py +0 -0
- {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/test/test_units.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lca_algebraic
|
|
3
|
-
Version: 1.4.1.
|
|
3
|
+
Version: 1.4.1.dev1
|
|
4
4
|
Summary: This library provides a layer above Brightway for defining parametric models and running super fast LCA for monte carlo analysis.
|
|
5
5
|
Author-email: OIE - Mines ParisTech <raphael.jolivet@mines-paristech.fr>
|
|
6
6
|
License: BSD
|
|
@@ -29,6 +29,7 @@ Requires-Dist: deprecation
|
|
|
29
29
|
Requires-Dist: SALib
|
|
30
30
|
Requires-Dist: pint
|
|
31
31
|
Requires-Dist: typing-extensions
|
|
32
|
+
Requires-Dist: xarray>=2022.3.0
|
|
32
33
|
Provides-Extra: bw2
|
|
33
34
|
Requires-Dist: brightway2; extra == "bw2"
|
|
34
35
|
Requires-Dist: pypardiso; platform_machine == "x86_64" and extra == "bw2"
|
|
@@ -168,13 +168,11 @@ class ActivityExtended(Activity):
|
|
|
168
168
|
or dict of attributes, for updating both at once, or any other attribute.
|
|
169
169
|
The amount can reference the symbol 'old_amount' that will be replaced with the current amount of the exchange.
|
|
170
170
|
"""
|
|
171
|
-
|
|
172
|
-
if not _isForeground(self["database"]):
|
|
171
|
+
if not _isForeground(self["database"]) and not Settings.internals:
|
|
173
172
|
msg = f"You are updating a background activity in {self['database']}. Use copyActivity() in a foreground db instead."
|
|
174
173
|
if Settings.strict_mode:
|
|
175
174
|
raise Exception(msg)
|
|
176
|
-
|
|
177
|
-
warn(msg)
|
|
175
|
+
warn(msg)
|
|
178
176
|
|
|
179
177
|
# Update exchanges
|
|
180
178
|
for ex_target_or_name, updates in updates.items():
|
|
@@ -541,12 +539,11 @@ def _equals(val1: ValueOrExpression, val2: ValueOrExpression):
|
|
|
541
539
|
|
|
542
540
|
|
|
543
541
|
def _newAct(db_name, code):
|
|
544
|
-
if not _isForeground(db_name):
|
|
542
|
+
if not _isForeground(db_name) and not Settings.internals:
|
|
545
543
|
msg = f"You are creating activity in background DB {db_name}. Use copyActivity() in a foreground db instead."
|
|
546
544
|
if Settings.strict_mode:
|
|
547
545
|
raise Exception(msg)
|
|
548
|
-
|
|
549
|
-
warn(msg)
|
|
546
|
+
warn(msg)
|
|
550
547
|
|
|
551
548
|
db = _getDb(db_name)
|
|
552
549
|
# Already present : delete it ?
|
|
@@ -632,7 +629,9 @@ def newActivity(
|
|
|
632
629
|
|
|
633
630
|
|
|
634
631
|
@atomic
|
|
635
|
-
def copyActivity(
|
|
632
|
+
def copyActivity(
|
|
633
|
+
db_name, activity: ActivityExtended, code=None, withExchanges=True, input_mapping=None, **kwargs
|
|
634
|
+
) -> ActivityExtended:
|
|
636
635
|
"""Copy an activity and its exchanges into another database. You usually want to copy activities from your background to
|
|
637
636
|
your foreground DB to update them, keeping your background DB clean.
|
|
638
637
|
|
|
@@ -675,6 +674,9 @@ def copyActivity(db_name, activity: ActivityExtended, code=None, withExchanges=T
|
|
|
675
674
|
data["output"] = res.key
|
|
676
675
|
if data["input"] == activity.key:
|
|
677
676
|
data["input"] = res.key
|
|
677
|
+
if input_mapping and data["input"] in input_mapping:
|
|
678
|
+
# Input mapping, used to migrate proxy db linked to premise scenarios
|
|
679
|
+
data["input"] = input_mapping[data["input"]]
|
|
678
680
|
|
|
679
681
|
# Chemical formulas might be in ecoinvent technosphere
|
|
680
682
|
# We don't want them
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from contextlib import AbstractContextManager
|
|
2
|
+
from functools import cache
|
|
2
3
|
from inspect import isfunction
|
|
3
4
|
from typing import Dict, Iterable, Tuple, Union
|
|
4
5
|
|
|
@@ -26,6 +27,20 @@ def _isnumber(value):
|
|
|
26
27
|
dbs = dict()
|
|
27
28
|
|
|
28
29
|
|
|
30
|
+
def invalidate_db(dbname=None):
|
|
31
|
+
"""Drop the pooled Database instances so a fresh one is built on next access.
|
|
32
|
+
|
|
33
|
+
bw25 keeps an internal (session/ORM) view inside a Database object that is not
|
|
34
|
+
invalidated when a database is deleted or recreated. Keep the pool in sync so that
|
|
35
|
+
activities are not resolved to stale (deleted) node ids.
|
|
36
|
+
"""
|
|
37
|
+
global dbs
|
|
38
|
+
if dbname is None:
|
|
39
|
+
dbs = dict()
|
|
40
|
+
else:
|
|
41
|
+
dbs.pop(dbname, None)
|
|
42
|
+
|
|
43
|
+
|
|
29
44
|
def _getDb(dbname):
|
|
30
45
|
"""Pool of Database instances"""
|
|
31
46
|
if dbname not in dbs:
|
|
@@ -174,6 +189,7 @@ def one(it: Iterable):
|
|
|
174
189
|
return it[0]
|
|
175
190
|
|
|
176
191
|
|
|
192
|
+
@cache
|
|
177
193
|
def getActByCode(db_name, code):
|
|
178
194
|
"""Get activity by code"""
|
|
179
195
|
return _getDb(db_name).get(code)
|
|
@@ -181,4 +197,4 @@ def getActByCode(db_name, code):
|
|
|
181
197
|
|
|
182
198
|
# Types
|
|
183
199
|
ValueOrExpression = Union[float, Basic, Quantity]
|
|
184
|
-
MethodKey = Tuple[str]
|
|
200
|
+
MethodKey = Tuple[str, ...]
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
from typing import Callable, Iterable, List, Tuple, Type
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
from .protocol import BrightwayBackend
|
|
6
|
+
|
|
7
|
+
_INSTALL_MSG = (
|
|
8
|
+
"Brightway is required. Install one of:\n" " pip install 'lca_algebraic[bw2]'\n" " pip install 'lca_algebraic[bw25]'"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _bw2data_major_version(bw2data) -> int:
|
|
13
|
+
version = bw2data.__version__
|
|
14
|
+
if isinstance(version, tuple):
|
|
15
|
+
return int(version[0])
|
|
16
|
+
return int(str(version).split(".")[0])
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _load_backend() -> BrightwayBackend:
|
|
20
|
+
try:
|
|
21
|
+
import bw2data
|
|
22
|
+
except ImportError as exc:
|
|
23
|
+
raise ImportError(_INSTALL_MSG) from exc
|
|
24
|
+
|
|
25
|
+
if _bw2data_major_version(bw2data) >= 4:
|
|
26
|
+
from .bw25 import BW25Backend
|
|
27
|
+
|
|
28
|
+
return BW25Backend()
|
|
29
|
+
|
|
30
|
+
from .bw2 import BW2Backend
|
|
31
|
+
|
|
32
|
+
return BW2Backend()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
_backend = _load_backend()
|
|
36
|
+
|
|
37
|
+
Activity = _backend.Activity
|
|
38
|
+
ActivityDataset = _backend.ActivityDataset
|
|
39
|
+
Exchange = _backend.Exchange
|
|
40
|
+
ExchangeDataset = _backend.ExchangeDataset
|
|
41
|
+
ActivityProxyBase = _backend.ActivityProxyBase
|
|
42
|
+
LCIBackend = _backend.LCIBackend
|
|
43
|
+
sqlite3_lci_db = _backend.sqlite3_lci_db
|
|
44
|
+
dict_as_exchangedataset = _backend.dict_as_exchangedataset
|
|
45
|
+
|
|
46
|
+
name = _backend.name
|
|
47
|
+
|
|
48
|
+
_LAZY_ATTRS = frozenset({"databases", "projects", "methods"})
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def __getattr__(attr: str):
|
|
52
|
+
if attr in _LAZY_ATTRS:
|
|
53
|
+
return getattr(_backend, attr)
|
|
54
|
+
raise AttributeError(f"module {__name__!r} has no attribute {attr!r}")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def Database(db_name: str):
|
|
58
|
+
return _backend.Database(db_name)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def Method(key: Tuple):
|
|
62
|
+
return _backend.Method(key)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_activity(key):
|
|
66
|
+
return _backend.get_activity(key)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def iter_database(db_name: str) -> Iterable:
|
|
70
|
+
return _backend.iter_database(db_name)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def multi_lca(
|
|
74
|
+
activities: List[dict],
|
|
75
|
+
methods: List[Tuple],
|
|
76
|
+
act_name_fn: Callable,
|
|
77
|
+
method_name_fn: Callable,
|
|
78
|
+
) -> pd.DataFrame:
|
|
79
|
+
return _backend.multi_lca(activities, methods, act_name_fn, method_name_fn)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def is_output_exchange(exc) -> bool:
|
|
83
|
+
return _backend.is_output_exchange(exc)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def exchange_type_for_sub_act(sub_act) -> str:
|
|
87
|
+
return _backend.exchange_type_for_sub_act(sub_act)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def process_node_default() -> str:
|
|
91
|
+
return _backend.process_node_default()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def production_edge_default() -> str:
|
|
95
|
+
return _backend.production_edge_default()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def activity_not_found() -> Type[Exception]:
|
|
99
|
+
return _backend.activity_not_found()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def new_database_parameters(params, db_name: str) -> None:
|
|
103
|
+
_backend.new_database_parameters(params, db_name)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def new_project_parameters(params) -> None:
|
|
107
|
+
_backend.new_project_parameters(params)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def copy_activity_skip_keys() -> set:
|
|
111
|
+
return _backend.copy_activity_skip_keys()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def bw2setup() -> None:
|
|
115
|
+
_backend.bw2setup()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def SingleOutputEcospold2Importer(path, dbname, use_mp=False):
|
|
119
|
+
return _backend.SingleOutputEcospold2Importer(path, dbname, use_mp=use_mp)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
__all__ = [
|
|
123
|
+
"Activity",
|
|
124
|
+
"ActivityProxyBase",
|
|
125
|
+
"BrightwayBackend",
|
|
126
|
+
"Database",
|
|
127
|
+
"Exchange",
|
|
128
|
+
"ExchangeDataset",
|
|
129
|
+
"LCIBackend",
|
|
130
|
+
"Method",
|
|
131
|
+
"SingleOutputEcospold2Importer",
|
|
132
|
+
"activity_not_found",
|
|
133
|
+
"bw2setup",
|
|
134
|
+
"copy_activity_skip_keys",
|
|
135
|
+
"databases",
|
|
136
|
+
"dict_as_exchangedataset",
|
|
137
|
+
"exchange_type_for_sub_act",
|
|
138
|
+
"get_activity",
|
|
139
|
+
"is_output_exchange",
|
|
140
|
+
"iter_database",
|
|
141
|
+
"methods",
|
|
142
|
+
"multi_lca",
|
|
143
|
+
"name",
|
|
144
|
+
"new_database_parameters",
|
|
145
|
+
"new_project_parameters",
|
|
146
|
+
"process_node_default",
|
|
147
|
+
"production_edge_default",
|
|
148
|
+
"projects",
|
|
149
|
+
"sqlite3_lci_db",
|
|
150
|
+
]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from typing import Callable, List, Tuple
|
|
2
|
+
|
|
3
|
+
import brightway2 as bw
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from bw2data.backends import LCIBackend
|
|
6
|
+
from bw2data.backends.peewee import (
|
|
7
|
+
Activity,
|
|
8
|
+
ActivityDataset,
|
|
9
|
+
Exchange,
|
|
10
|
+
ExchangeDataset,
|
|
11
|
+
sqlite3_lci_db,
|
|
12
|
+
)
|
|
13
|
+
from bw2data.backends.peewee.utils import dict_as_exchangedataset
|
|
14
|
+
from bw2data.proxies import ActivityProxyBase
|
|
15
|
+
from peewee import DoesNotExist
|
|
16
|
+
|
|
17
|
+
_TECHNO_TYPES = [None, "process", "product", "processwithreferenceproduct", "multifunctional"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BW2Backend:
|
|
21
|
+
name = "bw2"
|
|
22
|
+
|
|
23
|
+
Activity = Activity
|
|
24
|
+
ActivityDataset = ActivityDataset
|
|
25
|
+
Exchange = Exchange
|
|
26
|
+
ExchangeDataset = ExchangeDataset
|
|
27
|
+
ActivityProxyBase = ActivityProxyBase
|
|
28
|
+
LCIBackend = LCIBackend
|
|
29
|
+
sqlite3_lci_db = sqlite3_lci_db
|
|
30
|
+
dict_as_exchangedataset = staticmethod(dict_as_exchangedataset)
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def databases(self):
|
|
34
|
+
return bw.databases
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def projects(self):
|
|
38
|
+
return bw.projects
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def methods(self):
|
|
42
|
+
return bw.methods
|
|
43
|
+
|
|
44
|
+
def Database(self, name: str):
|
|
45
|
+
return bw.Database(name)
|
|
46
|
+
|
|
47
|
+
def Method(self, key: Tuple):
|
|
48
|
+
return bw.Method(key)
|
|
49
|
+
|
|
50
|
+
def get_activity(self, key):
|
|
51
|
+
return bw.get_activity(key)
|
|
52
|
+
|
|
53
|
+
def iter_database(self, db_name: str):
|
|
54
|
+
return iter(bw.Database(db_name))
|
|
55
|
+
|
|
56
|
+
def multi_lca(
|
|
57
|
+
self,
|
|
58
|
+
activities: List[dict],
|
|
59
|
+
methods: List[Tuple],
|
|
60
|
+
act_name_fn: Callable,
|
|
61
|
+
method_name_fn: Callable,
|
|
62
|
+
) -> pd.DataFrame:
|
|
63
|
+
bw.calculation_setups["process"] = {"inv": activities, "ia": methods}
|
|
64
|
+
lca = bw.MultiLCA("process")
|
|
65
|
+
cols = [act_name_fn(act) for act_amount in activities for act in act_amount]
|
|
66
|
+
return pd.DataFrame(lca.results.T, index=[method_name_fn(method) for method in methods], columns=cols)
|
|
67
|
+
|
|
68
|
+
def is_output_exchange(self, exc) -> bool:
|
|
69
|
+
return exc.get("type") == "production"
|
|
70
|
+
|
|
71
|
+
def exchange_type_for_sub_act(self, sub_act) -> str:
|
|
72
|
+
if sub_act.get("type") in _TECHNO_TYPES:
|
|
73
|
+
return "technosphere"
|
|
74
|
+
return "biosphere"
|
|
75
|
+
|
|
76
|
+
def process_node_default(self) -> str:
|
|
77
|
+
return "process"
|
|
78
|
+
|
|
79
|
+
def production_edge_default(self) -> str:
|
|
80
|
+
return "production"
|
|
81
|
+
|
|
82
|
+
def activity_not_found(self):
|
|
83
|
+
return DoesNotExist
|
|
84
|
+
|
|
85
|
+
def new_database_parameters(self, params, db_name: str) -> None:
|
|
86
|
+
bw.parameters.new_database_parameters(params, db_name)
|
|
87
|
+
|
|
88
|
+
def new_project_parameters(self, params) -> None:
|
|
89
|
+
bw.parameters.new_project_parameters(params)
|
|
90
|
+
|
|
91
|
+
def copy_activity_skip_keys(self) -> set:
|
|
92
|
+
return {"database", "code"}
|
|
93
|
+
|
|
94
|
+
def bw2setup(self):
|
|
95
|
+
bw.bw2setup()
|
|
96
|
+
|
|
97
|
+
def SingleOutputEcospold2Importer(self, path, dbname, use_mp=False):
|
|
98
|
+
return bw.SingleOutputEcospold2Importer(path, dbname, use_mp=use_mp)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from typing import Callable, List, Tuple
|
|
2
|
+
|
|
3
|
+
import bw2calc
|
|
4
|
+
import bw2data
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from bw2data import Database
|
|
8
|
+
from bw2data.backends import (
|
|
9
|
+
Activity,
|
|
10
|
+
ActivityDataset,
|
|
11
|
+
Exchange,
|
|
12
|
+
ExchangeDataset,
|
|
13
|
+
SQLiteBackend,
|
|
14
|
+
sqlite3_lci_db,
|
|
15
|
+
)
|
|
16
|
+
from bw2data.backends.utils import dict_as_exchangedataset
|
|
17
|
+
from bw2data.configuration import labels
|
|
18
|
+
from bw2data.errors import UnknownObject
|
|
19
|
+
from bw2data.parameters import ParameterManager
|
|
20
|
+
from bw2data.proxies import ActivityProxyBase
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BW25Backend:
|
|
24
|
+
name = "bw25"
|
|
25
|
+
|
|
26
|
+
Activity = Activity
|
|
27
|
+
ActivityDataset = ActivityDataset
|
|
28
|
+
Exchange = Exchange
|
|
29
|
+
ExchangeDataset = ExchangeDataset
|
|
30
|
+
ActivityProxyBase = ActivityProxyBase
|
|
31
|
+
LCIBackend = SQLiteBackend
|
|
32
|
+
sqlite3_lci_db = sqlite3_lci_db
|
|
33
|
+
dict_as_exchangedataset = staticmethod(dict_as_exchangedataset)
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def databases(self):
|
|
37
|
+
return bw2data.databases
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def projects(self):
|
|
41
|
+
return bw2data.projects
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def methods(self):
|
|
45
|
+
return bw2data.methods
|
|
46
|
+
|
|
47
|
+
def Database(self, name: str):
|
|
48
|
+
return Database(name)
|
|
49
|
+
|
|
50
|
+
def Method(self, key: Tuple):
|
|
51
|
+
return bw2data.Method(key)
|
|
52
|
+
|
|
53
|
+
def get_activity(self, key):
|
|
54
|
+
return bw2data.get_activity(key)
|
|
55
|
+
|
|
56
|
+
def iter_database(self, db_name: str):
|
|
57
|
+
return iter(Database(db_name))
|
|
58
|
+
|
|
59
|
+
def multi_lca(
|
|
60
|
+
self,
|
|
61
|
+
activities: List[dict],
|
|
62
|
+
methods: List[Tuple],
|
|
63
|
+
act_name_fn: Callable,
|
|
64
|
+
method_name_fn: Callable,
|
|
65
|
+
) -> pd.DataFrame:
|
|
66
|
+
meth_cfg = {"impact_categories": methods}
|
|
67
|
+
fu = {act_name_fn(act): {act.id: 1} for act_amount in activities for act in act_amount}
|
|
68
|
+
|
|
69
|
+
bw2data.databases.clean()
|
|
70
|
+
data_objs = bw2data.get_multilca_data_objs(fu, meth_cfg)
|
|
71
|
+
lca = bw2calc.MultiLCA(demands=fu, method_config=meth_cfg, data_objs=data_objs)
|
|
72
|
+
lca.lci()
|
|
73
|
+
lca.lcia()
|
|
74
|
+
|
|
75
|
+
rows = [method_name_fn(method) for method in methods]
|
|
76
|
+
cols = [act_name_fn(act) for act_amount in activities for act in act_amount]
|
|
77
|
+
results = lca.scores
|
|
78
|
+
|
|
79
|
+
return pd.DataFrame(
|
|
80
|
+
np.array([[results[m, a] for a in cols] for m in methods]),
|
|
81
|
+
index=rows,
|
|
82
|
+
columns=cols,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def is_output_exchange(self, exc) -> bool:
|
|
86
|
+
return exc.get("type") == labels.production_edge_default
|
|
87
|
+
|
|
88
|
+
def exchange_type_for_sub_act(self, sub_act) -> str:
|
|
89
|
+
if sub_act.get("type") in labels.lci_node_types:
|
|
90
|
+
return labels.consumption_edge_default
|
|
91
|
+
return labels.biosphere_edge_default
|
|
92
|
+
|
|
93
|
+
def process_node_default(self) -> str:
|
|
94
|
+
return labels.process_node_default
|
|
95
|
+
|
|
96
|
+
def production_edge_default(self) -> str:
|
|
97
|
+
return labels.production_edge_default
|
|
98
|
+
|
|
99
|
+
def activity_not_found(self):
|
|
100
|
+
return UnknownObject
|
|
101
|
+
|
|
102
|
+
def new_database_parameters(self, params, db_name: str) -> None:
|
|
103
|
+
ParameterManager().new_database_parameters(params, db_name)
|
|
104
|
+
|
|
105
|
+
def new_project_parameters(self, params) -> None:
|
|
106
|
+
ParameterManager().new_project_parameters(params)
|
|
107
|
+
|
|
108
|
+
def copy_activity_skip_keys(self) -> set:
|
|
109
|
+
return {"database", "code", "id"}
|
|
110
|
+
|
|
111
|
+
def bw2setup(self) -> None:
|
|
112
|
+
raise NotImplementedError("bw2setup is only supported with brightway2")
|
|
113
|
+
|
|
114
|
+
def SingleOutputEcospold2Importer(self, path, dbname, use_mp=False):
|
|
115
|
+
raise NotImplementedError("SingleOutputEcospold2Importer is only supported with brightway2")
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from typing import Any, Callable, Iterable, List, Protocol, Tuple, Type
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BrightwayBackend(Protocol):
|
|
7
|
+
"""Brightway 2 / 2.5 compatibility surface used by lca_algebraic."""
|
|
8
|
+
|
|
9
|
+
Activity: Any
|
|
10
|
+
Exchange: Any
|
|
11
|
+
ActivityDataset: Any
|
|
12
|
+
ExchangeDataset: Any
|
|
13
|
+
ActivityProxyBase: Any
|
|
14
|
+
LCIBackend: Any
|
|
15
|
+
sqlite3_lci_db: Any
|
|
16
|
+
dict_as_exchangedataset: Callable[..., Any]
|
|
17
|
+
|
|
18
|
+
name: str
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def databases(self) -> Any:
|
|
22
|
+
...
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def projects(self) -> Any:
|
|
26
|
+
...
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def methods(self) -> Iterable:
|
|
30
|
+
...
|
|
31
|
+
|
|
32
|
+
def Database(self, name: str) -> Any:
|
|
33
|
+
...
|
|
34
|
+
|
|
35
|
+
def Method(self, key: Tuple) -> Any:
|
|
36
|
+
...
|
|
37
|
+
|
|
38
|
+
def get_activity(self, key) -> Any:
|
|
39
|
+
...
|
|
40
|
+
|
|
41
|
+
def iter_database(self, db_name: str) -> Iterable:
|
|
42
|
+
...
|
|
43
|
+
|
|
44
|
+
def multi_lca(
|
|
45
|
+
self,
|
|
46
|
+
activities: List[dict],
|
|
47
|
+
methods: List[Tuple],
|
|
48
|
+
act_name_fn: Callable,
|
|
49
|
+
method_name_fn: Callable,
|
|
50
|
+
) -> pd.DataFrame:
|
|
51
|
+
...
|
|
52
|
+
|
|
53
|
+
def is_output_exchange(self, exc) -> bool:
|
|
54
|
+
...
|
|
55
|
+
|
|
56
|
+
def exchange_type_for_sub_act(self, sub_act) -> str:
|
|
57
|
+
...
|
|
58
|
+
|
|
59
|
+
def process_node_default(self) -> str:
|
|
60
|
+
...
|
|
61
|
+
|
|
62
|
+
def production_edge_default(self) -> str:
|
|
63
|
+
...
|
|
64
|
+
|
|
65
|
+
def activity_not_found(self) -> Type[Exception]:
|
|
66
|
+
...
|
|
67
|
+
|
|
68
|
+
def new_database_parameters(self, params, db_name: str) -> None:
|
|
69
|
+
...
|
|
70
|
+
|
|
71
|
+
def new_project_parameters(self, params) -> None:
|
|
72
|
+
...
|
|
73
|
+
|
|
74
|
+
def copy_activity_skip_keys(self) -> set:
|
|
75
|
+
...
|
|
76
|
+
|
|
77
|
+
def bw2setup(self) -> None:
|
|
78
|
+
...
|
|
79
|
+
|
|
80
|
+
def SingleOutputEcospold2Importer(self, path, dbname, use_mp=False) -> Any:
|
|
81
|
+
...
|
|
@@ -8,14 +8,15 @@ from typing import Dict, Tuple
|
|
|
8
8
|
from dill import Pickler, load
|
|
9
9
|
from sympy.core.function import UndefinedFunction
|
|
10
10
|
|
|
11
|
-
from lca_algebraic.bw_wrapper import databases, projects
|
|
11
|
+
from lca_algebraic.bw_wrapper import Database, databases, projects
|
|
12
12
|
|
|
13
13
|
from .database import _getMeta
|
|
14
|
-
from .log import
|
|
14
|
+
from .log import logger
|
|
15
15
|
from .settings import PROXY_DB_FLAG, Settings
|
|
16
16
|
|
|
17
17
|
LCIA_CACHE = "lcia"
|
|
18
18
|
EXPR_CACHE = "expr"
|
|
19
|
+
MAPPING_CACHE = "mapping"
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
# Overide the behaviour for pickling sympy.UndefineFunction
|
|
@@ -43,11 +44,15 @@ def flush_caches():
|
|
|
43
44
|
cache.sync()
|
|
44
45
|
|
|
45
46
|
|
|
46
|
-
def get_dependant_dbs(db_name):
|
|
47
|
+
def get_dependant_dbs(db_name, skip_proxy=True):
|
|
47
48
|
"""Recursively get list of dependant db names, including the current one"""
|
|
48
49
|
|
|
50
|
+
# Only work if database is up to date (processed)
|
|
51
|
+
if databases[db_name].get("dirty"):
|
|
52
|
+
Database(db_name).process()
|
|
53
|
+
|
|
49
54
|
# Skip proxy databases
|
|
50
|
-
if _getMeta(db_name, PROXY_DB_FLAG):
|
|
55
|
+
if skip_proxy and _getMeta(db_name, PROXY_DB_FLAG):
|
|
51
56
|
res = set()
|
|
52
57
|
else:
|
|
53
58
|
res = set([db_name])
|
|
@@ -150,7 +155,7 @@ class SyncDict(MutableMapping):
|
|
|
150
155
|
if self.last_update <= file_mtime:
|
|
151
156
|
return
|
|
152
157
|
|
|
153
|
-
info(f"Flushing cache {self.name} / {self.db_name}")
|
|
158
|
+
# info(f"Flushing cache {self.name} / {self.db_name}")
|
|
154
159
|
|
|
155
160
|
tmp = self._filename() + ".tmp"
|
|
156
161
|
with open(tmp, "wb") as f:
|
|
@@ -208,6 +213,11 @@ class ExprCache(_CacheDict):
|
|
|
208
213
|
super().__init__(EXPR_CACHE, db_name)
|
|
209
214
|
|
|
210
215
|
|
|
216
|
+
class MappingCache(_CacheDict):
|
|
217
|
+
def __init__(self, db_name):
|
|
218
|
+
super().__init__(MAPPING_CACHE, db_name)
|
|
219
|
+
|
|
220
|
+
|
|
211
221
|
def clear_caches(local=True, disk=True):
|
|
212
222
|
for cache_name in [LCIA_CACHE, EXPR_CACHE]:
|
|
213
223
|
for db_name in databases:
|
|
@@ -20,6 +20,7 @@ from lca_algebraic.bw_wrapper import (
|
|
|
20
20
|
sqlite3_lci_db,
|
|
21
21
|
)
|
|
22
22
|
|
|
23
|
+
from .base_utils import getActByCode, invalidate_db
|
|
23
24
|
from .log import logger
|
|
24
25
|
from .settings import PROXY_DB_FLAG
|
|
25
26
|
|
|
@@ -87,6 +88,15 @@ def resetDb(db_name, foreground=True):
|
|
|
87
88
|
logger.warning("Db %s was here. Reseting it" % db_name)
|
|
88
89
|
del dbmeta[db_name]
|
|
89
90
|
|
|
91
|
+
# Drop the pooled Database instance : it may hold a stale view (e.g. bw25 node ids)
|
|
92
|
+
# of a previously deleted/recreated database with the same name
|
|
93
|
+
invalidate_db(db_name)
|
|
94
|
+
|
|
95
|
+
# getActByCode() memoizes activity objects by (db_name, code). Activities are
|
|
96
|
+
# recreated with the same code on reset, so drop the memo to avoid returning
|
|
97
|
+
# stale (deleted) node ids.
|
|
98
|
+
getActByCode.cache_clear()
|
|
99
|
+
|
|
90
100
|
db = Database(db_name)
|
|
91
101
|
db.write(dict())
|
|
92
102
|
if foreground:
|
|
@@ -26,6 +26,7 @@ from lca_algebraic.base_utils import (
|
|
|
26
26
|
)
|
|
27
27
|
from lca_algebraic.bw_wrapper import (
|
|
28
28
|
Activity,
|
|
29
|
+
ActivityDataset,
|
|
29
30
|
Database,
|
|
30
31
|
activity_not_found,
|
|
31
32
|
databases,
|
|
@@ -35,9 +36,16 @@ from lca_algebraic.bw_wrapper import (
|
|
|
35
36
|
from lca_algebraic.lambda_expression import LambdaExpr
|
|
36
37
|
from lca_algebraic.settings import Settings
|
|
37
38
|
|
|
38
|
-
from .
|
|
39
|
+
from . import atomic, copyActivity, getActByCode, resetDb
|
|
40
|
+
from .cache import (
|
|
41
|
+
ExprCache,
|
|
42
|
+
LCIACache,
|
|
43
|
+
MappingCache,
|
|
44
|
+
get_dependant_dbs,
|
|
45
|
+
get_last_update,
|
|
46
|
+
)
|
|
39
47
|
from .database import BIOSPHERE_PREFIX, DbContext, _isForeground, _setMeta
|
|
40
|
-
from .log import info,
|
|
48
|
+
from .log import info, progress, warn
|
|
41
49
|
from .matrix import ActMatrix, invert
|
|
42
50
|
from .methods import method_name, method_unit
|
|
43
51
|
from .params import (
|
|
@@ -58,6 +66,10 @@ from .sympy_utils import replace_and_cleanup
|
|
|
58
66
|
IMPACTS_SYMBOL = IndexedBase("impacts")
|
|
59
67
|
|
|
60
68
|
|
|
69
|
+
# Mapping for activities for premise / scenarios
|
|
70
|
+
MAPPING_KEYS = ("name", "location", "unit", "reference product")
|
|
71
|
+
|
|
72
|
+
|
|
61
73
|
def register_user_function(sym, func):
|
|
62
74
|
"""Register a custom function with is python implementation
|
|
63
75
|
Parameters
|
|
@@ -165,7 +177,10 @@ def _multiLCAWithCache(all_acts, methods) -> Dict[Tuple[ActivityExtended, Method
|
|
|
165
177
|
)
|
|
166
178
|
|
|
167
179
|
if len(remaining_acts) > 0 and len(remaining_methods) > 0:
|
|
168
|
-
|
|
180
|
+
progress(
|
|
181
|
+
f"Computing brightway LCIA for {db_name}. "
|
|
182
|
+
f"{len(remaining_acts)} background acts on methods {remaining_methods}"
|
|
183
|
+
)
|
|
169
184
|
|
|
170
185
|
lca = _multiLCAWithProxies(remaining_acts, remaining_methods)
|
|
171
186
|
|
|
@@ -215,7 +230,7 @@ def _actToLambdaExpr(model: Activity, axis=None, alpha=1) -> LambdaExpr:
|
|
|
215
230
|
mset_key = _matrices_set_cache_key(base_key)
|
|
216
231
|
|
|
217
232
|
if mset_key not in cache.data:
|
|
218
|
-
|
|
233
|
+
progress(f"{db_name} matrices were not in expression cache, computing...")
|
|
219
234
|
if axis is None:
|
|
220
235
|
cache.data[mset_key] = AxisMatricesSet(variants={"_all_": _computeMatrices(db_name=db_name)})
|
|
221
236
|
else:
|
|
@@ -238,7 +253,7 @@ def lambdify_expr(expr):
|
|
|
238
253
|
|
|
239
254
|
|
|
240
255
|
def _preMultiLCAAlgebric(
|
|
241
|
-
model: ActivityExtended, methods: MethodKey, alpha: ValueOrExpression = 1, axis=None
|
|
256
|
+
model: ActivityExtended, methods: List[MethodKey], alpha: ValueOrExpression = 1, axis: str = None, scenario: list[str] = None
|
|
242
257
|
) -> list[LambdaExpr]:
|
|
243
258
|
"""
|
|
244
259
|
This method transforms an activity into a set of functions ready to compute LCA very fast on a set on methods.
|
|
@@ -252,12 +267,15 @@ def _preMultiLCAAlgebric(
|
|
|
252
267
|
alpha = alpha.magnitude
|
|
253
268
|
|
|
254
269
|
def _key(method):
|
|
255
|
-
|
|
270
|
+
key = (model, axis, method, alpha)
|
|
271
|
+
if scenario:
|
|
272
|
+
key += (scenario,)
|
|
273
|
+
return key
|
|
256
274
|
|
|
257
275
|
with ExprCache(model["database"]) as cache:
|
|
258
276
|
missing_methods = [method for method in methods if not _key(method) in cache.data]
|
|
259
277
|
if len(missing_methods) > 0:
|
|
260
|
-
exprs = _modelToExpr(model, methods=missing_methods, axis=axis, alpha=alpha)
|
|
278
|
+
exprs = _modelToExpr(model, methods=missing_methods, axis=axis, alpha=alpha, scenario=scenario)
|
|
261
279
|
for method, expr in zip(missing_methods, exprs):
|
|
262
280
|
cache.data[_key(method)] = expr
|
|
263
281
|
|
|
@@ -266,7 +284,150 @@ def _preMultiLCAAlgebric(
|
|
|
266
284
|
return list(cache.data[_key(method)] for method in methods)
|
|
267
285
|
|
|
268
286
|
|
|
269
|
-
def
|
|
287
|
+
def _build_mapping_key(act: Activity):
|
|
288
|
+
return tuple(act.get(attr) for attr in MAPPING_KEYS)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _get_dependant_scenario(db_name):
|
|
292
|
+
"""Get premise scenarios of which depend this database (exchanges pointing to other dbs)"""
|
|
293
|
+
res = set()
|
|
294
|
+
for target_db in get_dependant_dbs(db_name, skip_proxy=False):
|
|
295
|
+
if target_db == db_name or Settings.scenario_separator not in target_db:
|
|
296
|
+
continue
|
|
297
|
+
|
|
298
|
+
_, scenario = target_db.split(Settings.scenario_separator)
|
|
299
|
+
res.add(scenario)
|
|
300
|
+
|
|
301
|
+
if len(res) == 0:
|
|
302
|
+
return None
|
|
303
|
+
if len(res) == 1:
|
|
304
|
+
return list(res)[0]
|
|
305
|
+
|
|
306
|
+
raise Exception(f"Database {db_name} depends on more than one premise scenarios : {res}")
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _code_by_key(db_name: str) -> Dict[Tuple, str]:
|
|
310
|
+
"""List all activities of a db by their keys => code"""
|
|
311
|
+
|
|
312
|
+
master_key = "all_acts"
|
|
313
|
+
|
|
314
|
+
with MappingCache(db_name) as cache:
|
|
315
|
+
if master_key not in cache.data:
|
|
316
|
+
progress(f"Building cache of keys for {db_name}")
|
|
317
|
+
|
|
318
|
+
query = (
|
|
319
|
+
ActivityDataset.select(ActivityDataset.code, ActivityDataset.data)
|
|
320
|
+
.where(ActivityDataset.database == db_name)
|
|
321
|
+
.tuples()
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
mapping = dict()
|
|
325
|
+
for code, data in query:
|
|
326
|
+
key = _build_mapping_key(data)
|
|
327
|
+
if key in mapping:
|
|
328
|
+
raise Exception(f"Duplicate key {key} in {db_name}")
|
|
329
|
+
mapping[key] = code
|
|
330
|
+
|
|
331
|
+
cache.data[master_key] = mapping
|
|
332
|
+
|
|
333
|
+
return cache.data[master_key]
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _key_to_key_mapping(source_db, dest_db):
|
|
337
|
+
"""Return mapping of {(db, code) : (db, code)}"""
|
|
338
|
+
|
|
339
|
+
src_code_by_key = _code_by_key(source_db)
|
|
340
|
+
dest_code_by_key = _code_by_key(dest_db)
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
(source_db, src_code): (dest_db, dest_code_by_key[src_key])
|
|
344
|
+
for src_key, src_code in src_code_by_key.items()
|
|
345
|
+
if src_key in dest_code_by_key
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
@atomic
|
|
350
|
+
def _duplicate_db_for_scenario(origin_db, target_db):
|
|
351
|
+
"""Duplicate a proxy database and targets another scenario"""
|
|
352
|
+
|
|
353
|
+
# Empty the target
|
|
354
|
+
resetDb(target_db, foreground=False)
|
|
355
|
+
|
|
356
|
+
with temp_settings(internals=True):
|
|
357
|
+
target_scenario = target_db.split(Settings.scenario_separator)[1]
|
|
358
|
+
|
|
359
|
+
progress(f"Duplicating proxy database {origin_db} for IAM scenario {target_scenario}")
|
|
360
|
+
|
|
361
|
+
# Gather all mappings
|
|
362
|
+
all_mappings = dict()
|
|
363
|
+
for dependant_db in get_dependant_dbs(origin_db, skip_proxy=False):
|
|
364
|
+
if Settings.scenario_separator not in dependant_db or dependant_db == origin_db:
|
|
365
|
+
continue
|
|
366
|
+
|
|
367
|
+
dep_db_prefix, dep_scenario = dependant_db.split(Settings.scenario_separator)
|
|
368
|
+
dep_target_db = dep_db_prefix + Settings.scenario_separator + target_scenario
|
|
369
|
+
all_mappings.update(_key_to_key_mapping(dependant_db, dep_target_db))
|
|
370
|
+
|
|
371
|
+
# Loop on activities
|
|
372
|
+
for act in Database(origin_db):
|
|
373
|
+
copyActivity(db_name=target_db, activity=act, code=act["name"], input_mapping=all_mappings)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _map_activities(acts: list[Activity], source_db: str, target_db: str):
|
|
377
|
+
dependant_scenario = _get_dependant_scenario(source_db)
|
|
378
|
+
|
|
379
|
+
# Support for automatically creating or updating proxy db for other scenarios
|
|
380
|
+
if target_db not in databases:
|
|
381
|
+
# To dependancy : this is a root premise DB : missing scenrio should not happen
|
|
382
|
+
if dependant_scenario is None:
|
|
383
|
+
raise Exception(f"Missing target database for premise scenrio {target_db}")
|
|
384
|
+
|
|
385
|
+
info(f"Proxy database {target_db} was missing. Creating from {source_db}")
|
|
386
|
+
_duplicate_db_for_scenario(source_db, target_db)
|
|
387
|
+
|
|
388
|
+
# Exiisting db This is a proxy db
|
|
389
|
+
# Check it did not change
|
|
390
|
+
elif dependant_scenario is not None and get_last_update(source_db) > get_last_update(target_db):
|
|
391
|
+
info(f"Proxy database {source_db} changed. Updating {target_db}")
|
|
392
|
+
_duplicate_db_for_scenario(source_db, target_db)
|
|
393
|
+
|
|
394
|
+
code_by_key = _code_by_key(target_db)
|
|
395
|
+
|
|
396
|
+
def _get_target_act(act):
|
|
397
|
+
key = _build_mapping_key(act)
|
|
398
|
+
try:
|
|
399
|
+
code = code_by_key[key]
|
|
400
|
+
except KeyError:
|
|
401
|
+
key_dict = {k: v for k, v in zip(MAPPING_KEYS, key)}
|
|
402
|
+
raise Exception(f"Failed to find mapping for activity {key_dict} in target db {target_db}")
|
|
403
|
+
|
|
404
|
+
return getActByCode(target_db, code)
|
|
405
|
+
|
|
406
|
+
return {act: _get_target_act(act) for act in acts}
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _compute_scenario_mapping(acts: list[Activity], scenario: str = None):
|
|
410
|
+
"""In case a scenario is requested"""
|
|
411
|
+
|
|
412
|
+
res = dict()
|
|
413
|
+
|
|
414
|
+
for db_name, acts in _group_acts_by_db(acts).items():
|
|
415
|
+
# Not premise Db or no scenario requested ? => no mapping
|
|
416
|
+
if Settings.scenario_separator not in db_name or scenario is None:
|
|
417
|
+
# Map all activities to themselves
|
|
418
|
+
res.update({act: act for act in acts})
|
|
419
|
+
|
|
420
|
+
else:
|
|
421
|
+
prefix, source_scenario = db_name.split(Settings.scenario_separator)
|
|
422
|
+
target_db = prefix + Settings.scenario_separator + scenario
|
|
423
|
+
res.update(_map_activities(acts, db_name, target_db))
|
|
424
|
+
|
|
425
|
+
return res
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _modelToExpr(
|
|
429
|
+
model: Activity, methods: List[MethodKey], axis=None, alpha: ValueOrExpression = 1, scenario: str = None
|
|
430
|
+
) -> List[LambdaExpr]:
|
|
270
431
|
"""
|
|
271
432
|
Compute expressions corresponding to a model for each impact, replacing activities by the value of its impact
|
|
272
433
|
|
|
@@ -283,13 +444,17 @@ def _modelToExpr(model: Activity, methods: List[MethodKey], axis=None, alpha: Va
|
|
|
283
444
|
zero.impacts = []
|
|
284
445
|
return [zero] * len(methods)
|
|
285
446
|
|
|
447
|
+
# Possibly map bg activities to another target scenario db
|
|
448
|
+
act_mapping = _compute_scenario_mapping(generic_lambda_expr.background_activities, scenario=scenario)
|
|
449
|
+
|
|
286
450
|
# Compute LCA for background activities
|
|
287
|
-
all_impacts = _multiLCAWithCache(
|
|
451
|
+
all_impacts = _multiLCAWithCache(act_mapping.values(), methods)
|
|
288
452
|
|
|
289
453
|
res = list()
|
|
290
454
|
|
|
291
455
|
for method in methods:
|
|
292
|
-
impacts = {bg_act: all_impacts[
|
|
456
|
+
impacts = {bg_act: all_impacts[target_act, method] for bg_act, target_act in act_mapping.items()}
|
|
457
|
+
|
|
293
458
|
res.append(generic_lambda_expr.with_impacts(impacts))
|
|
294
459
|
|
|
295
460
|
return res
|
|
@@ -320,6 +485,12 @@ def _postMultiLCAAlgebric(methods, lambdas: List[LambdaExpr], with_params=False,
|
|
|
320
485
|
|
|
321
486
|
param_length = _compute_param_length(params)
|
|
322
487
|
|
|
488
|
+
if len(lambdas[0].impacts) > 0:
|
|
489
|
+
first_impact = lambdas[0].impacts[0]
|
|
490
|
+
if isinstance(first_impact, (list, np.ndarray)):
|
|
491
|
+
nb_scenarios = len(first_impact)
|
|
492
|
+
param_length = max(param_length, nb_scenarios)
|
|
493
|
+
|
|
323
494
|
# lambda are SymDict ?
|
|
324
495
|
# If use them as number of params
|
|
325
496
|
if lambdas[0].has_axis and len(lambdas[0].expr) > 1:
|
|
@@ -536,13 +707,49 @@ def compute_inventory(
|
|
|
536
707
|
return DataFrame(items)
|
|
537
708
|
|
|
538
709
|
|
|
710
|
+
def _merge_lambda_scenarios(lambdas: list[list[LambdaExpr]]):
|
|
711
|
+
"""
|
|
712
|
+
Merge impact vector from several lambda comming from several premise scenarios.
|
|
713
|
+
All the expressions are he same, only the vector of impact differs.
|
|
714
|
+
We merge several impact_vectors into a single vector of vectors
|
|
715
|
+
"""
|
|
716
|
+
|
|
717
|
+
# Ensure liust (from generators)
|
|
718
|
+
lambdas = list(lambdas)
|
|
719
|
+
|
|
720
|
+
# They are all the same for all scenarios except for the impacts
|
|
721
|
+
ref_lambdas = lambdas[0]
|
|
722
|
+
|
|
723
|
+
nb_scenarios = len(lambdas)
|
|
724
|
+
|
|
725
|
+
if nb_scenarios == 1:
|
|
726
|
+
return ref_lambdas
|
|
727
|
+
|
|
728
|
+
# Copy lambda of first scennario
|
|
729
|
+
res = []
|
|
730
|
+
for imethod, expr in enumerate(ref_lambdas):
|
|
731
|
+
expr = copy(expr)
|
|
732
|
+
res.append(expr)
|
|
733
|
+
expr.impacts = list()
|
|
734
|
+
for ibackground in range(len(expr.background_activities)):
|
|
735
|
+
impacts = np.array([lambdas[iscenario][imethod].impacts[ibackground] for iscenario in range(nb_scenarios)])
|
|
736
|
+
expr.impacts.append(impacts)
|
|
737
|
+
return res
|
|
738
|
+
|
|
739
|
+
|
|
539
740
|
def compute_impacts(
|
|
540
|
-
models
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
741
|
+
models: Union[
|
|
742
|
+
Activity,
|
|
743
|
+
List[Activity],
|
|
744
|
+
List[Tuple[Activity, float]],
|
|
745
|
+
Dict[Activity, float],
|
|
746
|
+
],
|
|
747
|
+
methods: Union[MethodKey, List[MethodKey]],
|
|
748
|
+
axis: str = None,
|
|
749
|
+
functional_unit: float = 1,
|
|
544
750
|
return_params=False,
|
|
545
751
|
description=None,
|
|
752
|
+
scenario: Union[str, list[str]] = None,
|
|
546
753
|
**params,
|
|
547
754
|
):
|
|
548
755
|
"""
|
|
@@ -562,7 +769,7 @@ def compute_impacts(
|
|
|
562
769
|
In case of several models, you cannot use list of parameters
|
|
563
770
|
|
|
564
771
|
methods :
|
|
565
|
-
|
|
772
|
+
Single method or list of methods / impacts to consider
|
|
566
773
|
|
|
567
774
|
axis:
|
|
568
775
|
Designates the name of a custom attribute of foreground activities.
|
|
@@ -577,6 +784,11 @@ def compute_impacts(
|
|
|
577
784
|
In the later case, all parameters should have the same number of values.
|
|
578
785
|
Paremeters that are not provided will have their default value set.
|
|
579
786
|
|
|
787
|
+
scenario:
|
|
788
|
+
Premise scenario to target, as suffix of database name (separated with # by default).
|
|
789
|
+
This may also be a list of scenarios, in which case the number of scenarios should be equal to the
|
|
790
|
+
length of other list parameters (if any).
|
|
791
|
+
|
|
580
792
|
functional_unit:
|
|
581
793
|
Quantity (static or Sympy formula) by which to divide impacts. Optional, 1 by default.
|
|
582
794
|
|
|
@@ -643,7 +855,11 @@ def compute_impacts(
|
|
|
643
855
|
if functional_unit != 1:
|
|
644
856
|
alpha = alpha / functional_unit
|
|
645
857
|
|
|
646
|
-
|
|
858
|
+
scenarios = scenario if isinstance(scenario, list) else [scenario]
|
|
859
|
+
|
|
860
|
+
lambdas = _merge_lambda_scenarios(
|
|
861
|
+
_preMultiLCAAlgebric(model, methods, scenario=scenario, alpha=alpha, axis=axis) for scenario in scenarios
|
|
862
|
+
)
|
|
647
863
|
|
|
648
864
|
unit: Optional[Unit] = functional_unit.units if isinstance(functional_unit, Quantity) else None
|
|
649
865
|
|
|
@@ -662,6 +878,16 @@ def compute_impacts(
|
|
|
662
878
|
# param with several values
|
|
663
879
|
list_params = {k: vals for k, vals in params.items() if isinstance(vals, list)}
|
|
664
880
|
|
|
881
|
+
if isinstance(scenario, list):
|
|
882
|
+
if len(list_params) > 0:
|
|
883
|
+
nb_params_values = len(list(list_params.values())[0])
|
|
884
|
+
if len(scenario) != nb_params_values:
|
|
885
|
+
raise Exception(
|
|
886
|
+
f"Number of scenarios {len(scenario)} different from param values length {nb_params_values} "
|
|
887
|
+
)
|
|
888
|
+
|
|
889
|
+
list_params["scenario"] = scenario
|
|
890
|
+
|
|
665
891
|
# Shapes the output / index according to the axis or multi param entry
|
|
666
892
|
if axis:
|
|
667
893
|
df[axis] = lambdas[0].axis_keys
|
|
@@ -721,9 +947,12 @@ def _isBioAct(act: ActivityExtended):
|
|
|
721
947
|
return (BIOSPHERE_PREFIX in db_name) or ("type" in act and act["type"] in ["emission", "natural resource"])
|
|
722
948
|
|
|
723
949
|
|
|
724
|
-
def _getOrCreateProxyDb(db_name):
|
|
950
|
+
def _getOrCreateProxyDb(db_name, scenario=None):
|
|
725
951
|
"""Init proxy db to biosphere if not done yet"""
|
|
726
952
|
proxyname = db_name + "-proxy"
|
|
953
|
+
if scenario:
|
|
954
|
+
proxyname += Settings.scenario_separator + scenario
|
|
955
|
+
|
|
727
956
|
if proxyname not in databases:
|
|
728
957
|
db = Database(proxyname)
|
|
729
958
|
db.write(dict())
|
|
@@ -732,9 +961,23 @@ def _getOrCreateProxyDb(db_name):
|
|
|
732
961
|
|
|
733
962
|
|
|
734
963
|
def _getOrCreateProxy(act: ActivityExtended, exchanges: dict[ActivityExtended, float]):
|
|
735
|
-
|
|
964
|
+
scenarios = set()
|
|
965
|
+
for target_act in exchanges.keys():
|
|
966
|
+
db_name = target_act["database"]
|
|
967
|
+
if Settings.scenario_separator in db_name:
|
|
968
|
+
scenario = db_name.split(Settings.scenario_separator)[1]
|
|
969
|
+
scenarios.add(scenario)
|
|
970
|
+
|
|
971
|
+
if len(scenarios) == 0:
|
|
972
|
+
scenario = None
|
|
973
|
+
elif len(scenarios) == 1:
|
|
974
|
+
scenario = list(scenarios)[0]
|
|
975
|
+
else:
|
|
976
|
+
raise Exception(f"Found several scenrios in activity to proxy : {scenarios}")
|
|
977
|
+
|
|
978
|
+
proxy_db = _getOrCreateProxyDb(act["database"], scenario=scenario)
|
|
736
979
|
proxy_code = act["code"] + "#proxy"
|
|
737
|
-
with temp_settings(
|
|
980
|
+
with temp_settings(internals=True):
|
|
738
981
|
try:
|
|
739
982
|
proxy = _getDb(proxy_db).get(proxy_code)
|
|
740
983
|
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import os
|
|
3
3
|
|
|
4
|
+
PROGRESS = 25
|
|
5
|
+
|
|
6
|
+
logging.addLevelName(PROGRESS, "PROGRESS")
|
|
7
|
+
|
|
4
8
|
LOGLEVEL = os.environ.get("LOGLEVEL", "INFO").upper()
|
|
5
9
|
# logging.basicConfig(level=LOGLEVEL, format="[%(levelname)s] %(message)s")
|
|
6
10
|
|
|
@@ -27,3 +31,7 @@ def warn(*args):
|
|
|
27
31
|
def info(*args):
|
|
28
32
|
msg = " ".join(str(item) for item in args)
|
|
29
33
|
logger.info(msg)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def progress(msg, *args):
|
|
37
|
+
logger._log(PROGRESS, msg, args)
|
|
@@ -938,9 +938,7 @@ class ParamRegistry:
|
|
|
938
938
|
if param.dbname in self.params[key]:
|
|
939
939
|
message = "[ParamRegistry] Param %s was already defined in '%s' " % (param.name, param.dbname or "<project>")
|
|
940
940
|
|
|
941
|
-
if Settings.param_overriding_enabled:
|
|
942
|
-
warn(message + " overriding")
|
|
943
|
-
else:
|
|
941
|
+
if not Settings.param_overriding_enabled:
|
|
944
942
|
raise Exception(message + "overriding disabled in Settings")
|
|
945
943
|
|
|
946
944
|
self.params[key][param.dbname] = param
|
|
@@ -28,6 +28,9 @@ class Settings:
|
|
|
28
28
|
# When activted, forbids any creation or update in background database
|
|
29
29
|
strict_mode: bool = False
|
|
30
30
|
|
|
31
|
+
# Set temporarly to true for internal tools not to trigger warnings
|
|
32
|
+
internals = False
|
|
33
|
+
|
|
31
34
|
# Use "Common Sub expression patterns" in sympy lambdify
|
|
32
35
|
cse_mode = CSE.DEFAULT
|
|
33
36
|
|
|
@@ -35,6 +38,10 @@ class Settings:
|
|
|
35
38
|
# If set to flase, you shoudl ensure flushing caches yourself
|
|
36
39
|
auto_flush = True
|
|
37
40
|
|
|
41
|
+
# Separator considered in database name for scenarios
|
|
42
|
+
# You may chose another one with several safer catacters like "__" or "-scenario-"
|
|
43
|
+
scenario_separator = "#"
|
|
44
|
+
|
|
38
45
|
|
|
39
46
|
# Flag used on Db to set it as proxy
|
|
40
47
|
PROXY_DB_FLAG = "isProxy"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lca_algebraic
|
|
3
|
-
Version: 1.4.1.
|
|
3
|
+
Version: 1.4.1.dev1
|
|
4
4
|
Summary: This library provides a layer above Brightway for defining parametric models and running super fast LCA for monte carlo analysis.
|
|
5
5
|
Author-email: OIE - Mines ParisTech <raphael.jolivet@mines-paristech.fr>
|
|
6
6
|
License: BSD
|
|
@@ -29,6 +29,7 @@ Requires-Dist: deprecation
|
|
|
29
29
|
Requires-Dist: SALib
|
|
30
30
|
Requires-Dist: pint
|
|
31
31
|
Requires-Dist: typing-extensions
|
|
32
|
+
Requires-Dist: xarray>=2022.3.0
|
|
32
33
|
Provides-Extra: bw2
|
|
33
34
|
Requires-Dist: brightway2; extra == "bw2"
|
|
34
35
|
Requires-Dist: pypardiso; platform_machine == "x86_64" and extra == "bw2"
|
|
@@ -24,8 +24,13 @@ lca_algebraic.egg-info/SOURCES.txt
|
|
|
24
24
|
lca_algebraic.egg-info/dependency_links.txt
|
|
25
25
|
lca_algebraic.egg-info/requires.txt
|
|
26
26
|
lca_algebraic.egg-info/top_level.txt
|
|
27
|
+
lca_algebraic/bw_wrapper/__init__.py
|
|
28
|
+
lca_algebraic/bw_wrapper/bw2.py
|
|
29
|
+
lca_algebraic/bw_wrapper/bw25.py
|
|
30
|
+
lca_algebraic/bw_wrapper/protocol.py
|
|
27
31
|
test/test_axis_dict.py
|
|
28
32
|
test/test_axis_matrices_cache.py
|
|
29
33
|
test/test_cache.py
|
|
34
|
+
test/test_premise.py
|
|
30
35
|
test/test_sympy.py
|
|
31
36
|
test/test_units.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "lca_algebraic"
|
|
7
|
-
version = "1.4.1.
|
|
7
|
+
version = "1.4.1.dev1"
|
|
8
8
|
|
|
9
9
|
description = "This library provides a layer above Brightway for defining parametric models and running super fast LCA for monte carlo analysis."
|
|
10
10
|
readme = "README.md"
|
|
@@ -38,6 +38,7 @@ dependencies = [
|
|
|
38
38
|
"SALib",
|
|
39
39
|
"pint",
|
|
40
40
|
"typing-extensions",
|
|
41
|
+
"xarray>=2022.3.0",
|
|
41
42
|
]
|
|
42
43
|
|
|
43
44
|
[project.optional-dependencies]
|
|
@@ -63,7 +64,7 @@ Documentation = "https://lca-algebraic.readthedocs.io/"
|
|
|
63
64
|
Changelog = "https://github.com/oie-mines-paristech/lca_algebraic/blob/main/RELEASE_NOTES.md"
|
|
64
65
|
|
|
65
66
|
[tool.setuptools]
|
|
66
|
-
packages = ["lca_algebraic"]
|
|
67
|
+
packages = ["lca_algebraic", "lca_algebraic.bw_wrapper"]
|
|
67
68
|
|
|
68
69
|
[tool.setuptools.package-data]
|
|
69
70
|
lca_algebraic = ["README.md"]
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from numpy.ma.testutils import assert_array_equal
|
|
2
|
+
|
|
3
|
+
from lca_algebraic import resetDb, newActivity, newFloatParam, compute_impacts, temp_settings
|
|
4
|
+
from test.conftest import DataFixture, USER_DB, assert_impacts
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_compute_impact_with_scenario(data: DataFixture):
|
|
8
|
+
# Scnerio database are separated with '#'
|
|
9
|
+
BG_DB1 = "bg#scen1"
|
|
10
|
+
BG_DB2 = "bg#scen2"
|
|
11
|
+
|
|
12
|
+
# Build two databases
|
|
13
|
+
resetDb(BG_DB1, foreground=False)
|
|
14
|
+
resetDb(BG_DB2, foreground=False)
|
|
15
|
+
|
|
16
|
+
# Add twin BG activities
|
|
17
|
+
bg_act = newActivity(BG_DB1, name="bg_act", unit="kg", exchanges={data.bio1: 1.0})
|
|
18
|
+
|
|
19
|
+
# Same activitiy with different exchange in scenario 2
|
|
20
|
+
bg_act2 = newActivity(BG_DB2, name="bg_act", unit="kg", exchanges={data.bio1: 2.0})
|
|
21
|
+
|
|
22
|
+
# Build parametrized model on BG1
|
|
23
|
+
p1 = newFloatParam("p1", default=1, min=0, max=1)
|
|
24
|
+
|
|
25
|
+
fg_act = newActivity(USER_DB, "fg", "kg", {bg_act: p1})
|
|
26
|
+
|
|
27
|
+
# Raw impacts without scenario : targeting scen1
|
|
28
|
+
res = compute_impacts(models=fg_act, methods=data.ibio1, p1=1.0)
|
|
29
|
+
|
|
30
|
+
assert_impacts(res, 1.0)
|
|
31
|
+
|
|
32
|
+
# Impact for scenario 2
|
|
33
|
+
res = compute_impacts(models=fg_act, methods=data.ibio1, scenario="scen2", p1=1.0)
|
|
34
|
+
|
|
35
|
+
assert_impacts(res, 2.0)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_compute_impact_with_several_scenarios(data: DataFixture):
|
|
39
|
+
# Scnerio database are separated with '#'
|
|
40
|
+
BG_DB1 = "bg#scen1"
|
|
41
|
+
BG_DB2 = "bg#scen2"
|
|
42
|
+
|
|
43
|
+
# Build two databases
|
|
44
|
+
resetDb(BG_DB1, foreground=False)
|
|
45
|
+
resetDb(BG_DB2, foreground=False)
|
|
46
|
+
|
|
47
|
+
# Add twin BG activities
|
|
48
|
+
bg_act = newActivity(BG_DB1, name="bg_act", unit="kg", exchanges={data.bio1: 1.0})
|
|
49
|
+
|
|
50
|
+
# Same activitiy with different exchange in scenario 2
|
|
51
|
+
bg_act2 = newActivity(BG_DB2, name="bg_act", unit="kg", exchanges={data.bio1: 2.0})
|
|
52
|
+
|
|
53
|
+
# Build parametrized model on BG1
|
|
54
|
+
p1 = newFloatParam("p1", default=1, min=0, max=1)
|
|
55
|
+
|
|
56
|
+
fg_act = newActivity(USER_DB, "fg", "kg", {bg_act: p1})
|
|
57
|
+
|
|
58
|
+
# Raw impacts without scenario : targeting scen1
|
|
59
|
+
res = compute_impacts(
|
|
60
|
+
models=fg_act,
|
|
61
|
+
methods=data.ibio1,
|
|
62
|
+
# p1=[1.0, 3.0],
|
|
63
|
+
scenario=["scen1", "scen2"],
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
assert_array_equal(res.values.flatten(), [1.0, 2.0])
|
|
67
|
+
|
|
68
|
+
# also vary parameter at the same time
|
|
69
|
+
res = compute_impacts(models=fg_act, methods=data.ibio1, p1=[1.0, 3.0], scenario=["scen1", "scen2"])
|
|
70
|
+
|
|
71
|
+
assert_array_equal(res.values.flatten(), [1.0, 6.0])
|
|
72
|
+
|
|
73
|
+
print(res)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_compute_impact_with_background_proxy(data: DataFixture):
|
|
77
|
+
"""Test the same with background proxy factorization activated"""
|
|
78
|
+
|
|
79
|
+
with temp_settings(factorize_static_bg=True, strict_mode=True):
|
|
80
|
+
# Scnerio database are separated with '#'
|
|
81
|
+
BG_DB1 = "bg#scen1"
|
|
82
|
+
BG_DB2 = "bg#scen2"
|
|
83
|
+
|
|
84
|
+
# Build two databases
|
|
85
|
+
resetDb(BG_DB1, foreground=False)
|
|
86
|
+
resetDb(BG_DB2, foreground=False)
|
|
87
|
+
|
|
88
|
+
with temp_settings(internals=True):
|
|
89
|
+
# Add twin BG activities
|
|
90
|
+
bg_act = newActivity(BG_DB1, name="bg_act", unit="kg", exchanges={data.bio1: 1.0})
|
|
91
|
+
bg_act2 = newActivity(BG_DB2, name="bg_act", unit="kg", exchanges={data.bio1: 2.0})
|
|
92
|
+
|
|
93
|
+
# Static amount so that backgrounf proxy is created
|
|
94
|
+
fg_act = newActivity(USER_DB, "fg", "kg", {bg_act: 1})
|
|
95
|
+
|
|
96
|
+
# Impact for scenario 2
|
|
97
|
+
res = compute_impacts(models=fg_act, methods=data.ibio1, scenario="scen2")
|
|
98
|
+
|
|
99
|
+
assert_impacts(res, 2.0)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|