lca-algebraic 1.4.1.dev3__tar.gz → 1.4.1.dev6__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 (44) hide show
  1. {lca_algebraic-1.4.1.dev3 → lca_algebraic-1.4.1.dev6}/PKG-INFO +2 -1
  2. {lca_algebraic-1.4.1.dev3 → lca_algebraic-1.4.1.dev6}/lca_algebraic/activity.py +49 -12
  3. {lca_algebraic-1.4.1.dev3 → lca_algebraic-1.4.1.dev6}/lca_algebraic/base_utils.py +18 -7
  4. {lca_algebraic-1.4.1.dev3 → lca_algebraic-1.4.1.dev6}/lca_algebraic/bw_wrapper/__init__.py +5 -0
  5. {lca_algebraic-1.4.1.dev3 → lca_algebraic-1.4.1.dev6}/lca_algebraic/bw_wrapper/bw2.py +10 -0
  6. {lca_algebraic-1.4.1.dev3 → lca_algebraic-1.4.1.dev6}/lca_algebraic/bw_wrapper/bw25.py +10 -0
  7. {lca_algebraic-1.4.1.dev3 → lca_algebraic-1.4.1.dev6}/lca_algebraic/bw_wrapper/protocol.py +3 -0
  8. {lca_algebraic-1.4.1.dev3 → lca_algebraic-1.4.1.dev6}/lca_algebraic/cache.py +18 -3
  9. {lca_algebraic-1.4.1.dev3 → lca_algebraic-1.4.1.dev6}/lca_algebraic/lambda_expression.py +28 -1
  10. {lca_algebraic-1.4.1.dev3 → lca_algebraic-1.4.1.dev6}/lca_algebraic/lca.py +175 -28
  11. {lca_algebraic-1.4.1.dev3 → lca_algebraic-1.4.1.dev6}/lca_algebraic/matrix.py +38 -13
  12. {lca_algebraic-1.4.1.dev3 → lca_algebraic-1.4.1.dev6}/lca_algebraic/methods.py +9 -7
  13. {lca_algebraic-1.4.1.dev3 → lca_algebraic-1.4.1.dev6}/lca_algebraic/params.py +83 -17
  14. {lca_algebraic-1.4.1.dev3 → lca_algebraic-1.4.1.dev6}/lca_algebraic/stats.py +123 -16
  15. {lca_algebraic-1.4.1.dev3 → lca_algebraic-1.4.1.dev6}/lca_algebraic/sympy_utils.py +1 -3
  16. {lca_algebraic-1.4.1.dev3 → lca_algebraic-1.4.1.dev6}/lca_algebraic.egg-info/PKG-INFO +2 -1
  17. {lca_algebraic-1.4.1.dev3 → lca_algebraic-1.4.1.dev6}/lca_algebraic.egg-info/SOURCES.txt +6 -0
  18. {lca_algebraic-1.4.1.dev3 → lca_algebraic-1.4.1.dev6}/lca_algebraic.egg-info/requires.txt +1 -0
  19. {lca_algebraic-1.4.1.dev3 → lca_algebraic-1.4.1.dev6}/pyproject.toml +3 -2
  20. lca_algebraic-1.4.1.dev6/test/test_expand_params.py +26 -0
  21. lca_algebraic-1.4.1.dev6/test/test_methods.py +35 -0
  22. lca_algebraic-1.4.1.dev6/test/test_overwrite.py +73 -0
  23. lca_algebraic-1.4.1.dev6/test/test_params.py +90 -0
  24. lca_algebraic-1.4.1.dev6/test/test_proxy_cache.py +47 -0
  25. lca_algebraic-1.4.1.dev6/test/test_stats.py +99 -0
  26. {lca_algebraic-1.4.1.dev3 → lca_algebraic-1.4.1.dev6}/test/test_units.py +71 -7
  27. {lca_algebraic-1.4.1.dev3 → lca_algebraic-1.4.1.dev6}/LICENSE +0 -0
  28. {lca_algebraic-1.4.1.dev3 → lca_algebraic-1.4.1.dev6}/README.md +0 -0
  29. {lca_algebraic-1.4.1.dev3 → lca_algebraic-1.4.1.dev6}/lca_algebraic/__init__.py +0 -0
  30. {lca_algebraic-1.4.1.dev3 → lca_algebraic-1.4.1.dev6}/lca_algebraic/axis_dict.py +0 -0
  31. {lca_algebraic-1.4.1.dev3 → lca_algebraic-1.4.1.dev6}/lca_algebraic/database.py +0 -0
  32. {lca_algebraic-1.4.1.dev3 → lca_algebraic-1.4.1.dev6}/lca_algebraic/interpolation.py +0 -0
  33. {lca_algebraic-1.4.1.dev3 → lca_algebraic-1.4.1.dev6}/lca_algebraic/io.py +0 -0
  34. {lca_algebraic-1.4.1.dev3 → lca_algebraic-1.4.1.dev6}/lca_algebraic/log.py +0 -0
  35. {lca_algebraic-1.4.1.dev3 → lca_algebraic-1.4.1.dev6}/lca_algebraic/settings.py +0 -0
  36. {lca_algebraic-1.4.1.dev3 → lca_algebraic-1.4.1.dev6}/lca_algebraic/units.py +0 -0
  37. {lca_algebraic-1.4.1.dev3 → lca_algebraic-1.4.1.dev6}/lca_algebraic.egg-info/dependency_links.txt +0 -0
  38. {lca_algebraic-1.4.1.dev3 → lca_algebraic-1.4.1.dev6}/lca_algebraic.egg-info/top_level.txt +0 -0
  39. {lca_algebraic-1.4.1.dev3 → lca_algebraic-1.4.1.dev6}/setup.cfg +0 -0
  40. {lca_algebraic-1.4.1.dev3 → lca_algebraic-1.4.1.dev6}/test/test_axis_dict.py +0 -0
  41. {lca_algebraic-1.4.1.dev3 → lca_algebraic-1.4.1.dev6}/test/test_axis_matrices_cache.py +0 -0
  42. {lca_algebraic-1.4.1.dev3 → lca_algebraic-1.4.1.dev6}/test/test_cache.py +0 -0
  43. {lca_algebraic-1.4.1.dev3 → lca_algebraic-1.4.1.dev6}/test/test_premise.py +0 -0
  44. {lca_algebraic-1.4.1.dev3 → lca_algebraic-1.4.1.dev6}/test/test_sympy.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lca_algebraic
3
- Version: 1.4.1.dev3
3
+ Version: 1.4.1.dev6
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
@@ -41,6 +41,7 @@ Requires-Dist: pre-commit; extra == "dev"
41
41
  Requires-Dist: pytest; extra == "dev"
42
42
  Requires-Dist: nbmake; extra == "dev"
43
43
  Requires-Dist: build; extra == "dev"
44
+ Requires-Dist: twine>=6.1; extra == "dev"
44
45
  Dynamic: license-file
45
46
 
46
47
 
@@ -13,12 +13,14 @@ from lca_algebraic.bw_wrapper import (
13
13
  Activity,
14
14
  Exchange,
15
15
  ExchangeDataset,
16
+ activity_not_found,
16
17
  copy_activity_skip_keys,
17
18
  dict_as_exchangedataset,
18
19
  exchange_type_for_sub_act,
19
20
  get_activity,
20
21
  process_node_default,
21
22
  production_edge_default,
23
+ purge_activity_content,
22
24
  )
23
25
  from lca_algebraic.database import (
24
26
  _find_biosphere_db,
@@ -29,6 +31,7 @@ from lca_algebraic.database import (
29
31
  )
30
32
  from lca_algebraic.params import (
31
33
  STORE_FORMULA_KEY,
34
+ STORE_FORMULA_TAG,
32
35
  DbContext,
33
36
  ParamDef,
34
37
  _complete_and_expand_params,
@@ -167,6 +170,11 @@ class ActivityExtended(Activity):
167
170
  or activity for updating only input,
168
171
  or dict of attributes, for updating both at once, or any other attribute.
169
172
  The amount can reference the symbol 'old_amount' that will be replaced with the current amount of the exchange.
173
+
174
+ Passing a dict of attributes is also the way to tag a single exchange with an axis \
175
+ (see ``compute_impacts``) : only that exchange will be ventilated under the category::
176
+
177
+ myActivity.updateExchanges({some_act: dict(amount=2, phase="transport")})
170
178
  """
171
179
  if not _isForeground(self["database"]) and not Settings.internals:
172
180
  msg = f"You are updating a background activity in {self['database']}. Use copyActivity() in a foreground db instead."
@@ -318,6 +326,9 @@ class ActivityExtended(Activity):
318
326
  def _amount_to_formula(self, amount: ValueOrExpression, exchange: ExchangeDataset):
319
327
  res = dict()
320
328
  if isinstance(amount, Basic):
329
+ if amount.is_number:
330
+ return {"amount": float(amount.evalf())}
331
+
321
332
  current_amount = exchange.get("amount", None)
322
333
  if current_amount is not None:
323
334
  amount = amount.subs(old_amount, current_amount)
@@ -329,8 +340,9 @@ class ActivityExtended(Activity):
329
340
  raise Exception("Symbol '%s' not found in params : %s" % (symbol, all_symbols))
330
341
 
331
342
  res[STORE_FORMULA_KEY] = str(amount)
343
+ res[STORE_FORMULA_TAG] = True
332
344
  res["amount"] = 0
333
- elif isinstance(amount, float) or isinstance(amount, int):
345
+ elif isinstance(amount, float | int):
334
346
  res["amount"] = amount
335
347
  else:
336
348
  raise Exception(
@@ -349,6 +361,9 @@ class ActivityExtended(Activity):
349
361
  # Update units
350
362
  amount = self._transform_unit(amount, exchange["unit"])
351
363
 
364
+ updates.pop(STORE_FORMULA_TAG, None)
365
+ exchange.pop(STORE_FORMULA_TAG, None)
366
+
352
367
  # Extract formula if two separate field "amount" and "formula"
353
368
  # Update the list of updates
354
369
  updates.update(self._amount_to_formula(amount, exchange))
@@ -392,7 +407,14 @@ class ActivityExtended(Activity):
392
407
  yield exch
393
408
 
394
409
  def updateMeta(self, **kwargs):
395
- """Update any property. Useful to update axes"""
410
+ """Update any custom property of the activity.
411
+
412
+ This is typically used to tag an activity with an axis : all its outgoing
413
+ exchanges will then be ventilated under that category by ``compute_impacts``::
414
+
415
+ myActivity.updateMeta(phase="manufacturing")
416
+ compute_impacts(model, methods, axis="phase")
417
+ """
396
418
  for key, val in kwargs.items():
397
419
  self._data[key] = val
398
420
  self.save()
@@ -475,6 +497,11 @@ def findActivity(
475
497
  search = search.lower()
476
498
  search = search.replace(",", " ")
477
499
 
500
+ # bw2data >= 4 backs `search` with FTS5, which rejects blank queries
501
+ # while bw2data 3 silently returned nothing.
502
+ if not search.strip():
503
+ return []
504
+
478
505
  # Find candidates via index
479
506
  # candidates = _find_candidates(db_name, name_key)
480
507
  candidates = _getDb(db_name).search(search, limit=limit)
@@ -482,7 +509,8 @@ def findActivity(
482
509
  if len(candidates) == 0:
483
510
  # Try again removing strange caracters
484
511
  search = re.sub(r"\w*[^a-zA-Z ]+\w*", " ", search)
485
- candidates = _getDb(db_name).search(search, limit=limit)
512
+ if search.strip():
513
+ candidates = _getDb(db_name).search(search, limit=limit)
486
514
 
487
515
  # Exact match
488
516
  return list(filter(act_filter, candidates))
@@ -546,13 +574,19 @@ def _newAct(db_name, code):
546
574
  warn(msg)
547
575
 
548
576
  db = _getDb(db_name)
549
- # Already present : delete it ?
550
- for act in db:
551
- if act["code"] == code:
552
- warn("Activity '%s' was already in '%s'. Overwriting it" % (code, db_name))
553
- act.delete()
554
577
 
555
- return db.new_activity(code)
578
+ try:
579
+ act = getActByCode(db_name, code)
580
+ except activity_not_found():
581
+ return db.new_activity(code)
582
+
583
+ # Already present : overwrite it in place.
584
+ # We must not delete+recreate the activity : on brightway 2.5, deleting an
585
+ # activity also removes the incoming links (exchanges from other activities
586
+ # consuming it), which would silently break previously built models.
587
+ warn("Activity '%s' was already in '%s'. Overwriting it" % (code, db_name))
588
+ purge_activity_content(act)
589
+ return act
556
590
 
557
591
 
558
592
  def newActivity(
@@ -630,7 +664,7 @@ def newActivity(
630
664
 
631
665
  @atomic
632
666
  def copyActivity(
633
- db_name, activity: ActivityExtended, code=None, withExchanges=True, input_mapping=None, **kwargs
667
+ db_name, activity: ActivityExtended, code=None, withExchanges=True, stripFormula=True, input_mapping=None, **kwargs
634
668
  ) -> ActivityExtended:
635
669
  """Copy an activity and its exchanges into another database. You usually want to copy activities from your background to
636
670
  your foreground DB to update them, keeping your background DB clean.
@@ -643,7 +677,10 @@ def copyActivity(
643
677
  Source activity
644
678
  code:
645
679
  Code of the target activity. Also used as its name
646
-
680
+ withExchange:
681
+ Copy exchanges is True (default), otherwise do not copy them
682
+ stripFormula:
683
+ keep exchanges formula if True, default is False
647
684
 
648
685
  Returns
649
686
  -------
@@ -680,7 +717,7 @@ def copyActivity(
680
717
 
681
718
  # Chemical formulas might be in ecoinvent technosphere
682
719
  # We don't want them
683
- if "formula" in data:
720
+ if stripFormula and ("formula" in data) and (STORE_FORMULA_TAG not in data):
684
721
  del data["formula"]
685
722
  ExchangeDataset.create(**dict_as_exchangedataset(data))
686
723
 
@@ -3,14 +3,16 @@ from functools import cache
3
3
  from inspect import isfunction
4
4
  from typing import Dict, Iterable, Tuple, Union
5
5
 
6
+ import sympy
6
7
  import ipywidgets as widgets
7
8
  import numpy as np
8
9
  import pandas as pd
9
10
  from IPython.display import display
10
11
  from six import raise_from
11
12
  from sympy import Basic
12
- from sympy.physics.units import Quantity
13
+ from pint import Quantity
13
14
 
15
+ from lca_algebraic.settings import Settings
14
16
  from lca_algebraic.bw_wrapper import Activity, Database, is_output_exchange
15
17
 
16
18
  _user_functions = dict()
@@ -48,14 +50,23 @@ def _getDb(dbname):
48
50
  return dbs[dbname]
49
51
 
50
52
 
51
- def Max(a, b):
52
- """Max define as algrebraic forumal with 'abs' for proper computation on vectors"""
53
- return (a + b + abs(a - b)) / 2
53
+ def _MinMax(op, arg0, *args):
54
+ from lca_algebraic.units import unit_registry as u
54
55
 
56
+ if not Settings.units_enabled:
57
+ return op(arg0, *args)
58
+ same_unit = all(x.units == arg0.units for x in args)
59
+ if not same_unit:
60
+ raise Exception("MinMax argments must have the same units")
61
+ return u.Quantity(op(arg0.magnitude, *[x.magnitude for x in args]), arg0.units)
55
62
 
56
- def Min(a, b):
57
- """Max define as algrebraic forumal with 'abs' for proper computation on vectors"""
58
- return (a + b - abs(b - a)) / 2
63
+
64
+ def Max(arg0, *args):
65
+ return _MinMax(sympy.Max, arg0, *args)
66
+
67
+
68
+ def Min(arg0, *args):
69
+ return _MinMax(sympy.Min, arg0, *args)
59
70
 
60
71
 
61
72
  def _actDesc(act: Activity):
@@ -111,6 +111,10 @@ def copy_activity_skip_keys() -> set:
111
111
  return _backend.copy_activity_skip_keys()
112
112
 
113
113
 
114
+ def purge_activity_content(act) -> None:
115
+ _backend.purge_activity_content(act)
116
+
117
+
114
118
  def bw2setup() -> None:
115
119
  _backend.bw2setup()
116
120
 
@@ -146,5 +150,6 @@ __all__ = [
146
150
  "process_node_default",
147
151
  "production_edge_default",
148
152
  "projects",
153
+ "purge_activity_content",
149
154
  "sqlite3_lci_db",
150
155
  ]
@@ -91,6 +91,16 @@ class BW2Backend:
91
91
  def copy_activity_skip_keys(self) -> set:
92
92
  return {"database", "code"}
93
93
 
94
+ def purge_activity_content(self, act) -> None:
95
+ """Remove an activity's outgoing exchanges and non-identity attributes.
96
+
97
+ The activity row, its code and the incoming links (exchanges of other
98
+ activities pointing to it) are preserved.
99
+ """
100
+ act.exchanges().delete()
101
+ for key in [k for k in act._data if k not in {"database", "code"}]:
102
+ del act._data[key]
103
+
94
104
  def bw2setup(self):
95
105
  bw.bw2setup()
96
106
 
@@ -108,6 +108,16 @@ class BW25Backend:
108
108
  def copy_activity_skip_keys(self) -> set:
109
109
  return {"database", "code", "id"}
110
110
 
111
+ def purge_activity_content(self, act) -> None:
112
+ """Remove an activity's outgoing exchanges and non-identity attributes.
113
+
114
+ The activity row, its code and the incoming links (exchanges of other
115
+ activities pointing to it) are preserved.
116
+ """
117
+ act.exchanges().delete(allow_in_sourced_project=True)
118
+ for key in [k for k in act._data if k not in {"database", "code", "id"}]:
119
+ del act._data[key]
120
+
111
121
  def bw2setup(self) -> None:
112
122
  raise NotImplementedError("bw2setup is only supported with brightway2")
113
123
 
@@ -74,6 +74,9 @@ class BrightwayBackend(Protocol):
74
74
  def copy_activity_skip_keys(self) -> set:
75
75
  ...
76
76
 
77
+ def purge_activity_content(self, act) -> None:
78
+ ...
79
+
77
80
  def bw2setup(self) -> None:
78
81
  ...
79
82
 
@@ -1,5 +1,6 @@
1
1
  import os
2
2
  import time
3
+ from base64 import b32encode
3
4
  from collections.abc import MutableMapping
4
5
  from datetime import datetime
5
6
  from os import path
@@ -19,6 +20,10 @@ EXPR_CACHE = "expr"
19
20
  MAPPING_CACHE = "mapping"
20
21
 
21
22
 
23
+ def _filename_encode(s: str):
24
+ return b32encode(s.encode("utf-8")).decode("utf-8")
25
+
26
+
22
27
  # Overide the behaviour for pickling sympy.UndefineFunction
23
28
  class MyPickler(Pickler):
24
29
  def reducer_override(self, obj):
@@ -44,8 +49,16 @@ def flush_caches():
44
49
  cache.sync()
45
50
 
46
51
 
47
- def get_dependant_dbs(db_name, skip_proxy=True):
48
- """Recursively get list of dependant db names, including the current one"""
52
+ def get_dependant_dbs(db_name, skip_proxy=None):
53
+ """Recursively get list of dependant db names, including the current one
54
+
55
+ Proxy databases are skipped unless background factorization is enabled : with
56
+ ``factorize_static_bg`` the developed expressions / LCIA of a database refer to
57
+ the acts of its ``<db>-proxy`` databases, so a change of those proxies must be
58
+ visible (see the staleness check in ``_CacheDict``).
59
+ """
60
+ if skip_proxy is None:
61
+ skip_proxy = not Settings.factorize_static_bg
49
62
 
50
63
  # Only work if database is up to date (processed)
51
64
  if databases[db_name].get("dirty"):
@@ -89,7 +102,9 @@ class SyncDict(MutableMapping):
89
102
  self.load()
90
103
 
91
104
  def _filename(self):
92
- return path.join(projects.dir, f"lca_algebraic_cache-{self.name}-{self.db_name}.pickle")
105
+ return path.join(
106
+ projects.dir, f"lca_algebraic_cache-{_filename_encode(self.name)}-{_filename_encode(self.db_name)}.pickle"
107
+ )
93
108
 
94
109
  # ---------- core MutableMapping ----------
95
110
  def __getitem__(self, key):
@@ -3,6 +3,7 @@ from dataclasses import dataclass
3
3
  from typing import Dict, List, Optional
4
4
 
5
5
  from sympy import Basic, Expr, lambdify, parse_expr
6
+ from sympy.core import S
6
7
  from sympy.printing.numpy import NumPyPrinter
7
8
 
8
9
  from lca_algebraic.axis_dict import AxisDict
@@ -19,6 +20,32 @@ from lca_algebraic.settings import CSE, Settings
19
20
  from lca_algebraic.sympy_utils import find_cses, find_symbols
20
21
 
21
22
 
23
+ class NumPyPrinterExt(NumPyPrinter):
24
+ def __init__(self, settings=None):
25
+ super().__init__(settings=settings)
26
+
27
+ def _helper_piecewise(self, *args):
28
+ from sympy.logic.boolalg import ITE, simplify_logic
29
+
30
+ def print_cond(cond):
31
+ """Problem having an ITE in the cond."""
32
+ if cond.has(ITE):
33
+ return self._print(simplify_logic(cond))
34
+ else:
35
+ return self._print(cond)
36
+
37
+ return "{}({},{},{})".format(
38
+ self._module_format(self._module + ".where"),
39
+ print_cond(args[0].cond),
40
+ self._print(args[0].expr),
41
+ self._helper_piecewise(*args[1:]) if len(args) > 1 else self._print(S.NaN),
42
+ )
43
+
44
+ def _print_Piecewise(self, expr):
45
+ "Piecewise function printer"
46
+ return self._helper_piecewise(*expr.args)
47
+
48
+
22
49
  @dataclass
23
50
  class ValueContext:
24
51
  """Represents a result value, with all parameters values used in context"""
@@ -172,7 +199,7 @@ class LambdWrapper:
172
199
  def _lambdify(expr: Basic, expanded_params):
173
200
  """Lambdify, handling manually the case of SymDict (for impacts by axis)"""
174
201
 
175
- printer = NumPyPrinter(
202
+ printer = NumPyPrinterExt(
176
203
  {"fully_qualified_modules": False, "inline": True, "allow_unknown_functions": True, "user_functions": dict()}
177
204
  )
178
205