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.
Files changed (38) hide show
  1. {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/PKG-INFO +2 -1
  2. {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic/activity.py +10 -8
  3. {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic/base_utils.py +17 -1
  4. lca_algebraic-1.4.1.dev1/lca_algebraic/bw_wrapper/__init__.py +150 -0
  5. lca_algebraic-1.4.1.dev1/lca_algebraic/bw_wrapper/bw2.py +98 -0
  6. lca_algebraic-1.4.1.dev1/lca_algebraic/bw_wrapper/bw25.py +115 -0
  7. lca_algebraic-1.4.1.dev1/lca_algebraic/bw_wrapper/protocol.py +81 -0
  8. {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic/cache.py +15 -5
  9. {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic/database.py +10 -0
  10. {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic/lca.py +262 -19
  11. {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic/log.py +8 -0
  12. {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic/params.py +1 -3
  13. {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic/settings.py +7 -0
  14. {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic.egg-info/PKG-INFO +2 -1
  15. {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic.egg-info/SOURCES.txt +5 -0
  16. {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic.egg-info/requires.txt +1 -0
  17. {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/pyproject.toml +3 -2
  18. lca_algebraic-1.4.1.dev1/test/test_premise.py +99 -0
  19. {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/LICENSE +0 -0
  20. {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/README.md +0 -0
  21. {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic/__init__.py +0 -0
  22. {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic/axis_dict.py +0 -0
  23. {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic/interpolation.py +0 -0
  24. {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic/io.py +0 -0
  25. {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic/lambda_expression.py +0 -0
  26. {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic/matrix.py +0 -0
  27. {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic/methods.py +0 -0
  28. {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic/stats.py +0 -0
  29. {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic/sympy_utils.py +0 -0
  30. {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic/units.py +0 -0
  31. {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic.egg-info/dependency_links.txt +0 -0
  32. {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/lca_algebraic.egg-info/top_level.txt +0 -0
  33. {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/setup.cfg +0 -0
  34. {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/test/test_axis_dict.py +0 -0
  35. {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/test/test_axis_matrices_cache.py +0 -0
  36. {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/test/test_cache.py +0 -0
  37. {lca_algebraic-1.4.1.dev0 → lca_algebraic-1.4.1.dev1}/test/test_sympy.py +0 -0
  38. {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.dev0
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
- else:
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
- else:
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(db_name, activity: ActivityExtended, code=None, withExchanges=True, **kwargs) -> ActivityExtended:
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 info, logger
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 .cache import ExprCache, LCIACache
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, logger, warn
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
- info(f"Computing LCA for {len(remaining_acts)} background acts on methods {remaining_methods}")
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
- logger.debug(f"{db_name} matrices were not in expression cache, computing...")
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
- return (model, axis, method, alpha)
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 _modelToExpr(model: Activity, methods: List[MethodKey], axis=None, alpha: ValueOrExpression = 1) -> List[LambdaExpr]:
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(generic_lambda_expr.background_activities, methods)
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[bg_act, method] for bg_act in generic_lambda_expr.background_activities}
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
- methods,
542
- axis=None,
543
- functional_unit=1,
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
- List of methods / impacts to consider
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
- lambdas = _preMultiLCAAlgebric(model, methods, alpha=alpha, axis=axis)
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
- proxy_db = _getOrCreateProxyDb(act["database"])
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(strict_mode=False):
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.dev0
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
@@ -10,6 +10,7 @@ deprecation
10
10
  SALib
11
11
  pint
12
12
  typing-extensions
13
+ xarray>=2022.3.0
13
14
 
14
15
  [bw2]
15
16
  brightway2
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "lca_algebraic"
7
- version = "1.4.1.dev0"
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)