headwater 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- headwater/__init__.py +107 -0
- headwater/_lazy.py +33 -0
- headwater/calc/__init__.py +76 -0
- headwater/calc/arrays.py +45 -0
- headwater/calc/cancellation.py +85 -0
- headwater/calc/errors.py +54 -0
- headwater/calc/explain.py +351 -0
- headwater/calc/monte_carlo.py +581 -0
- headwater/calc/signs.py +99 -0
- headwater/calc/skus.py +42 -0
- headwater/calc/solve.py +498 -0
- headwater/calc/study.py +101 -0
- headwater/calc/update.py +378 -0
- headwater/calc/weighting.py +56 -0
- headwater/cli/__init__.py +151 -0
- headwater/cli/_common.py +528 -0
- headwater/cli/brightway.py +96 -0
- headwater/cli/demo.py +38 -0
- headwater/cli/diff.py +270 -0
- headwater/cli/excel.py +154 -0
- headwater/cli/find.py +97 -0
- headwater/cli/method.py +211 -0
- headwater/cli/release.py +155 -0
- headwater/cli/schema.py +35 -0
- headwater/cli/sensitivity.py +101 -0
- headwater/cli/study.py +979 -0
- headwater/cli/upgrade.py +316 -0
- headwater/cli/uses.py +112 -0
- headwater/cli/where.py +40 -0
- headwater/demo/README.md +25 -0
- headwater/demo/releases/toy-background@6864b7dbb65d/exchanges.parquet +0 -0
- headwater/demo/releases/toy-background@6864b7dbb65d/flow_properties.parquet +0 -0
- headwater/demo/releases/toy-background@6864b7dbb65d/flows.parquet +0 -0
- headwater/demo/releases/toy-background@6864b7dbb65d/geographies.parquet +0 -0
- headwater/demo/releases/toy-background@6864b7dbb65d/issues.parquet +0 -0
- headwater/demo/releases/toy-background@6864b7dbb65d/methods/toy-climate/factors.parquet +0 -0
- headwater/demo/releases/toy-background@6864b7dbb65d/processes.parquet +0 -0
- headwater/demo/releases/toy-background@6864b7dbb65d/release.json +75 -0
- headwater/demo/releases/toy-background@6864b7dbb65d/units.parquet +0 -0
- headwater/demo/studies/dyes/database.yaml +3 -0
- headwater/demo/studies/dyes/flows.csv +2 -0
- headwater/demo/studies/dyes/parameters.yaml +5 -0
- headwater/demo/studies/dyes/processes/reactive-dye.csv +4 -0
- headwater/demo/studies/dyes/processes/reactive-dye.yaml +3 -0
- headwater/demo/studies/tshirt/database.yaml +3 -0
- headwater/demo/studies/tshirt/flows.csv +2 -0
- headwater/demo/studies/tshirt/processes/tshirt.csv +4 -0
- headwater/demo/studies/tshirt/processes/tshirt.yaml +3 -0
- headwater/demo/studies/tshirt-dyed/database.yaml +4 -0
- headwater/demo/studies/tshirt-dyed/flows.csv +2 -0
- headwater/demo/studies/tshirt-dyed/processes/tshirt.csv +5 -0
- headwater/demo/studies/tshirt-dyed/processes/tshirt.yaml +3 -0
- headwater/demo/studies/tshirt-footprint/scenarios/cleaner-dye.yaml +5 -0
- headwater/demo/studies/tshirt-footprint/study.yaml +8 -0
- headwater/demo/studies/tshirt-local-electricity/database.yaml +3 -0
- headwater/demo/studies/tshirt-local-electricity/flows.csv +2 -0
- headwater/demo/studies/tshirt-local-electricity/overrides/own-wind-power.csv +2 -0
- headwater/demo/studies/tshirt-local-electricity/processes/tshirt.csv +4 -0
- headwater/demo/studies/tshirt-local-electricity/processes/tshirt.yaml +3 -0
- headwater/diff/__init__.py +51 -0
- headwater/diff/_attribute.py +663 -0
- headwater/diff/_changes.py +1127 -0
- headwater/diff/_git.py +389 -0
- headwater/diff/_moved.py +44 -0
- headwater/diff/_releases.py +284 -0
- headwater/diff/_render.py +564 -0
- headwater/diff/_study.py +283 -0
- headwater/io/__init__.py +6 -0
- headwater/io/allocate.py +327 -0
- headwater/io/brightway/__init__.py +20 -0
- headwater/io/brightway/importer.py +134 -0
- headwater/io/brightway/project.py +465 -0
- headwater/io/brightway/reader.py +282 -0
- headwater/io/ecospold1/__init__.py +87 -0
- headwater/io/ecospold1/assemble.py +282 -0
- headwater/io/ecospold1/dataset.py +403 -0
- headwater/io/ecospold1/reader.py +162 -0
- headwater/io/ecospold1/roles.py +165 -0
- headwater/io/ecospold1/uncertainty.py +180 -0
- headwater/io/ecospold1/units.py +46 -0
- headwater/io/ecospold2/__init__.py +57 -0
- headwater/io/ecospold2/dataset.py +715 -0
- headwater/io/ecospold2/geographies.py +59 -0
- headwater/io/ecospold2/masterdata.py +324 -0
- headwater/io/ecospold2/reader.py +382 -0
- headwater/io/excel/__init__.py +52 -0
- headwater/io/excel/activities.py +187 -0
- headwater/io/excel/convert.py +279 -0
- headwater/io/excel/database_yaml.py +217 -0
- headwater/io/excel/importer.py +219 -0
- headwater/io/excel/index.py +152 -0
- headwater/io/excel/lines.py +480 -0
- headwater/io/excel/linking.py +322 -0
- headwater/io/excel/mapping.py +198 -0
- headwater/io/excel/parameters.py +164 -0
- headwater/io/excel/records.py +172 -0
- headwater/io/excel/report.py +257 -0
- headwater/io/excel/uncertainty.py +145 -0
- headwater/io/excel/units.py +89 -0
- headwater/io/excel/workbook.py +524 -0
- headwater/io/excel/write.py +224 -0
- headwater/io/exclude.py +105 -0
- headwater/io/files.py +25 -0
- headwater/io/formats.py +122 -0
- headwater/io/inventory.py +269 -0
- headwater/io/lcia/__init__.py +49 -0
- headwater/io/lcia/reader.py +138 -0
- headwater/io/lcia/workbook.py +413 -0
- headwater/io/method.py +412 -0
- headwater/io/nwset.py +109 -0
- headwater/io/olca_jsonld/__init__.py +26 -0
- headwater/io/olca_jsonld/flows.py +194 -0
- headwater/io/olca_jsonld/inventory.py +413 -0
- headwater/io/olca_jsonld/method.py +201 -0
- headwater/io/olca_jsonld/package.py +185 -0
- headwater/io/simapro_csv/__init__.py +43 -0
- headwater/io/simapro_csv/blocks.py +146 -0
- headwater/io/simapro_csv/export.py +162 -0
- headwater/io/simapro_csv/header.py +111 -0
- headwater/io/simapro_csv/method.py +129 -0
- headwater/io/simapro_csv/reader.py +297 -0
- headwater/io/simapro_csv/records.py +573 -0
- headwater/io/simapro_csv/text.py +40 -0
- headwater/io/simapro_csv/units.py +40 -0
- headwater/io/source.py +279 -0
- headwater/io/units.py +199 -0
- headwater/link/__init__.py +42 -0
- headwater/link/by_id.py +92 -0
- headwater/link/by_name.py +248 -0
- headwater/link/flows.py +113 -0
- headwater/link/lines.py +192 -0
- headwater/link/mapping.py +241 -0
- headwater/model/__init__.py +112 -0
- headwater/model/_types.py +17 -0
- headwater/model/catalog.py +155 -0
- headwater/model/database.py +117 -0
- headwater/model/errors.py +122 -0
- headwater/model/flows.py +63 -0
- headwater/model/formula.py +561 -0
- headwater/model/issue.py +81 -0
- headwater/model/method.py +224 -0
- headwater/model/nwset.py +217 -0
- headwater/model/override.py +106 -0
- headwater/model/parameters.py +320 -0
- headwater/model/process.py +232 -0
- headwater/model/roles.py +36 -0
- headwater/model/scenario.py +147 -0
- headwater/model/study.py +71 -0
- headwater/model/uncertainty.py +112 -0
- headwater/model/units.py +134 -0
- headwater/py.typed +0 -0
- headwater/query/__init__.py +80 -0
- headwater/query/_git.py +28 -0
- headwater/query/_hash.py +66 -0
- headwater/query/_internals.py +29 -0
- headwater/query/_open.py +27 -0
- headwater/query/_release.py +488 -0
- headwater/query/_scenario.py +248 -0
- headwater/query/_study_draws.py +482 -0
- headwater/query/_system.py +418 -0
- headwater/query/_uncertainty.py +175 -0
- headwater/query/background.py +1019 -0
- headwater/query/database.py +866 -0
- headwater/query/monte_carlo.py +617 -0
- headwater/query/result.py +1133 -0
- headwater/query/search.py +335 -0
- headwater/query/skus.py +279 -0
- headwater/query/study.py +607 -0
- headwater/release/__init__.py +51 -0
- headwater/release/cache.py +186 -0
- headwater/release/checks.py +1016 -0
- headwater/release/content.py +245 -0
- headwater/release/indicators.py +162 -0
- headwater/release/manifest.py +103 -0
- headwater/release/read.py +380 -0
- headwater/release/remote.py +159 -0
- headwater/release/schema.py +408 -0
- headwater/release/write.py +518 -0
- headwater/study/__init__.py +44 -0
- headwater/study/_cut_offs.py +264 -0
- headwater/study/_exchanges.py +207 -0
- headwater/study/_files.py +324 -0
- headwater/study/_no_factor.py +107 -0
- headwater/study/_overrides.py +117 -0
- headwater/study/_parameters.py +206 -0
- headwater/study/_scenarios.py +480 -0
- headwater/study/_study.py +308 -0
- headwater/study/_uncertainty.py +118 -0
- headwater/study/_uses.py +199 -0
- headwater/study/_write.py +361 -0
- headwater/study/edit.py +934 -0
- headwater/study/folders.py +168 -0
- headwater/study/reader.py +625 -0
- headwater/study/schema.py +171 -0
- headwater/study/validate.py +686 -0
- headwater/upgrade/__init__.py +30 -0
- headwater/upgrade/_datasets.py +133 -0
- headwater/upgrade/_flags.py +132 -0
- headwater/upgrade/_moves.py +103 -0
- headwater/upgrade/_plan.py +466 -0
- headwater/upgrade/_rules.py +214 -0
- headwater/upgrade/_write.py +213 -0
- headwater-0.1.0.dist-info/METADATA +150 -0
- headwater-0.1.0.dist-info/RECORD +207 -0
- headwater-0.1.0.dist-info/WHEEL +4 -0
- headwater-0.1.0.dist-info/entry_points.txt +3 -0
- headwater-0.1.0.dist-info/licenses/LICENSE +202 -0
headwater/__init__.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
from headwater._lazy import lazy_exports
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
__version__: str
|
|
7
|
+
from headwater.model import HeadwaterError, InvalidDataError
|
|
8
|
+
from headwater.query import (
|
|
9
|
+
Background,
|
|
10
|
+
Contribution,
|
|
11
|
+
Database,
|
|
12
|
+
Emitter,
|
|
13
|
+
ManyScores,
|
|
14
|
+
Match,
|
|
15
|
+
MonteCarlo,
|
|
16
|
+
MonteCarloProvenance,
|
|
17
|
+
NotCharacterised,
|
|
18
|
+
ParameterSensitivity,
|
|
19
|
+
Place,
|
|
20
|
+
Provenance,
|
|
21
|
+
Range,
|
|
22
|
+
Result,
|
|
23
|
+
Sensitivity,
|
|
24
|
+
Study,
|
|
25
|
+
Where,
|
|
26
|
+
load,
|
|
27
|
+
monte_carlo,
|
|
28
|
+
open,
|
|
29
|
+
score_many,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
_exported, _dir = lazy_exports(
|
|
33
|
+
globals(),
|
|
34
|
+
{
|
|
35
|
+
"headwater.model": ["HeadwaterError", "InvalidDataError"],
|
|
36
|
+
# From the module itself: once `headwater.query.monte_carlo` is imported, the attribute
|
|
37
|
+
# `monte_carlo` of `headwater.query` is that module, not the function.
|
|
38
|
+
"headwater.query.monte_carlo": [
|
|
39
|
+
"MonteCarlo",
|
|
40
|
+
"MonteCarloProvenance",
|
|
41
|
+
"Range",
|
|
42
|
+
"monte_carlo",
|
|
43
|
+
],
|
|
44
|
+
"headwater.query": [
|
|
45
|
+
"Background",
|
|
46
|
+
"Emitter",
|
|
47
|
+
"Contribution",
|
|
48
|
+
"Database",
|
|
49
|
+
"Sensitivity",
|
|
50
|
+
"Match",
|
|
51
|
+
"NotCharacterised",
|
|
52
|
+
"ParameterSensitivity",
|
|
53
|
+
"Place",
|
|
54
|
+
"Provenance",
|
|
55
|
+
"Result",
|
|
56
|
+
"ManyScores",
|
|
57
|
+
"Study",
|
|
58
|
+
"Where",
|
|
59
|
+
"load",
|
|
60
|
+
"open",
|
|
61
|
+
"score_many",
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def __getattr__(name: str) -> object:
|
|
68
|
+
"""The lazy exports, and `__version__`, read once from the installed metadata: pyproject.toml
|
|
69
|
+
is the one place that writes the version. `import headwater` loads no `importlib.metadata`."""
|
|
70
|
+
if name == "__version__":
|
|
71
|
+
from importlib.metadata import version
|
|
72
|
+
|
|
73
|
+
globals()["__version__"] = found = version("headwater")
|
|
74
|
+
return found
|
|
75
|
+
return _exported(name)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def __dir__() -> list[str]:
|
|
79
|
+
return sorted({*_dir(), "__version__"})
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
__all__ = [
|
|
83
|
+
"Background",
|
|
84
|
+
"Emitter",
|
|
85
|
+
"Contribution",
|
|
86
|
+
"Database",
|
|
87
|
+
"HeadwaterError",
|
|
88
|
+
"InvalidDataError",
|
|
89
|
+
"Sensitivity",
|
|
90
|
+
"Match",
|
|
91
|
+
"MonteCarlo",
|
|
92
|
+
"MonteCarloProvenance",
|
|
93
|
+
"NotCharacterised",
|
|
94
|
+
"ParameterSensitivity",
|
|
95
|
+
"Place",
|
|
96
|
+
"Provenance",
|
|
97
|
+
"Range",
|
|
98
|
+
"Result",
|
|
99
|
+
"ManyScores",
|
|
100
|
+
"Study",
|
|
101
|
+
"Where",
|
|
102
|
+
"__version__",
|
|
103
|
+
"load",
|
|
104
|
+
"monte_carlo",
|
|
105
|
+
"open",
|
|
106
|
+
"score_many",
|
|
107
|
+
]
|
headwater/_lazy.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Lazy exports for a package's `__init__`: `from headwater import open` loads `headwater.query`
|
|
2
|
+
only when `open` is first asked for, so importing the package, or a light submodule such as the
|
|
3
|
+
command line, loads no numpy, scipy or pyarrow."""
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def lazy_exports(
|
|
11
|
+
namespace: dict[str, Any], modules: Mapping[str, Sequence[str]]
|
|
12
|
+
) -> tuple[Callable[[str], object], Callable[[], list[str]]]:
|
|
13
|
+
"""The `__getattr__` and `__dir__` of a package, given its `globals()`.
|
|
14
|
+
|
|
15
|
+
`modules` maps a module to the names the package exports from it. Write the same imports under
|
|
16
|
+
`if TYPE_CHECKING:` in the package, so editors and pyright see the names, and set
|
|
17
|
+
`__getattr__, __dir__ = lazy_exports(globals(), {...})`.
|
|
18
|
+
"""
|
|
19
|
+
package = namespace["__name__"]
|
|
20
|
+
defined_in = {name: module for module, names in modules.items() for name in names}
|
|
21
|
+
|
|
22
|
+
def __getattr__(name: str) -> object:
|
|
23
|
+
module = defined_in.get(name)
|
|
24
|
+
if module is None:
|
|
25
|
+
raise AttributeError(f"module {package!r} has no attribute {name!r}")
|
|
26
|
+
value = getattr(importlib.import_module(module), name)
|
|
27
|
+
namespace[name] = value
|
|
28
|
+
return value
|
|
29
|
+
|
|
30
|
+
def __dir__() -> list[str]:
|
|
31
|
+
return sorted({*namespace, *defined_in})
|
|
32
|
+
|
|
33
|
+
return __getattr__, __dir__
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Pure calculation functions on NumPy arrays and SciPy sparse matrices.
|
|
2
|
+
|
|
3
|
+
Matrix indices exist only inside one calculation. No file access, no ids, no global state.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from headwater.calc.errors import CalculationError, ResidualError, SingularMatrixError
|
|
7
|
+
from headwater.calc.explain import (
|
|
8
|
+
Levers,
|
|
9
|
+
LineEffects,
|
|
10
|
+
Node,
|
|
11
|
+
emission_levers,
|
|
12
|
+
emission_line_levers,
|
|
13
|
+
explain,
|
|
14
|
+
levers,
|
|
15
|
+
line_levers,
|
|
16
|
+
where,
|
|
17
|
+
)
|
|
18
|
+
from headwater.calc.signs import Entry, Matrix, RoleEntry, matrix_entry, role_entries
|
|
19
|
+
from headwater.calc.skus import sku_scores
|
|
20
|
+
from headwater.calc.solve import (
|
|
21
|
+
CONDITION_LIMIT,
|
|
22
|
+
TOLERANCE,
|
|
23
|
+
Factorization,
|
|
24
|
+
Solution,
|
|
25
|
+
SolverSettings,
|
|
26
|
+
Step,
|
|
27
|
+
factorize,
|
|
28
|
+
scores,
|
|
29
|
+
supply,
|
|
30
|
+
)
|
|
31
|
+
from headwater.calc.study import StudySolution, solve_study
|
|
32
|
+
from headwater.calc.update import (
|
|
33
|
+
LowRankUpdate,
|
|
34
|
+
low_rank_update,
|
|
35
|
+
update_column,
|
|
36
|
+
update_emissions,
|
|
37
|
+
)
|
|
38
|
+
from headwater.calc.weighting import weighted_scores, weighted_sum
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"CONDITION_LIMIT",
|
|
42
|
+
"TOLERANCE",
|
|
43
|
+
"CalculationError",
|
|
44
|
+
"Entry",
|
|
45
|
+
"Factorization",
|
|
46
|
+
"Levers",
|
|
47
|
+
"LineEffects",
|
|
48
|
+
"LowRankUpdate",
|
|
49
|
+
"Matrix",
|
|
50
|
+
"Node",
|
|
51
|
+
"ResidualError",
|
|
52
|
+
"RoleEntry",
|
|
53
|
+
"SingularMatrixError",
|
|
54
|
+
"Solution",
|
|
55
|
+
"SolverSettings",
|
|
56
|
+
"Step",
|
|
57
|
+
"StudySolution",
|
|
58
|
+
"emission_levers",
|
|
59
|
+
"emission_line_levers",
|
|
60
|
+
"explain",
|
|
61
|
+
"factorize",
|
|
62
|
+
"levers",
|
|
63
|
+
"line_levers",
|
|
64
|
+
"low_rank_update",
|
|
65
|
+
"matrix_entry",
|
|
66
|
+
"role_entries",
|
|
67
|
+
"scores",
|
|
68
|
+
"sku_scores",
|
|
69
|
+
"solve_study",
|
|
70
|
+
"supply",
|
|
71
|
+
"update_column",
|
|
72
|
+
"update_emissions",
|
|
73
|
+
"weighted_scores",
|
|
74
|
+
"weighted_sum",
|
|
75
|
+
"where",
|
|
76
|
+
]
|
headwater/calc/arrays.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Input checks shared by the calculation functions."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from numpy.typing import ArrayLike, NDArray
|
|
5
|
+
from scipy import sparse
|
|
6
|
+
|
|
7
|
+
from headwater.calc.errors import CalculationError
|
|
8
|
+
|
|
9
|
+
type Vector = NDArray[np.float64]
|
|
10
|
+
type MatrixLike = sparse.sparray | sparse.spmatrix | NDArray[np.float64]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def as_matrix(A: MatrixLike, name: str = "matrix", *, copy: bool = True) -> sparse.csc_array:
|
|
14
|
+
"""A float64 CSC copy of `A`, with duplicate entries summed and zeros removed.
|
|
15
|
+
|
|
16
|
+
With `copy=False`, a matrix already in that form is returned as it is, not copied: for a
|
|
17
|
+
caller that only reads it. `A` itself is never changed. Raises CalculationError naming the
|
|
18
|
+
columns (processes) that hold a NaN or an infinity.
|
|
19
|
+
"""
|
|
20
|
+
matrix = sparse.csc_array(A, dtype=np.float64, copy=copy)
|
|
21
|
+
if matrix.ndim != 2:
|
|
22
|
+
raise ValueError(f"{name} must be 2-dimensional, got shape {matrix.shape}")
|
|
23
|
+
if copy or not matrix.has_canonical_format or not np.all(matrix.data):
|
|
24
|
+
if not copy:
|
|
25
|
+
matrix = matrix.copy()
|
|
26
|
+
matrix.sum_duplicates()
|
|
27
|
+
matrix.eliminate_zeros()
|
|
28
|
+
bad = ~np.isfinite(matrix.data)
|
|
29
|
+
if bad.any():
|
|
30
|
+
columns = np.repeat(np.arange(matrix.shape[1]), np.diff(matrix.indptr))
|
|
31
|
+
raise CalculationError(
|
|
32
|
+
f"{name} has entries that are not finite in columns", np.unique(columns[bad])
|
|
33
|
+
)
|
|
34
|
+
return matrix
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def as_vector(v: ArrayLike, n: int, name: str) -> Vector:
|
|
38
|
+
"""A float64 copy of `v`, checked to be finite and of length `n`."""
|
|
39
|
+
vector = np.array(v, dtype=np.float64)
|
|
40
|
+
if vector.shape != (n,):
|
|
41
|
+
raise ValueError(f"{name} must be a vector of length {n}, got shape {vector.shape}")
|
|
42
|
+
bad = np.flatnonzero(~np.isfinite(vector))
|
|
43
|
+
if bad.size:
|
|
44
|
+
raise CalculationError(f"{name} has entries that are not finite at indices", bad)
|
|
45
|
+
return vector
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""The positive and negative parts of a score, and whether the score is near 0 because they cancel.
|
|
2
|
+
|
|
3
|
+
Example, the carbon toy: 1 kg of fabric takes 1.5 kg of CO2 from the air, which counts -1.5,
|
|
4
|
+
and its residue emits 0.4 kg of non-fossil CO2, which counts +0.4. The net biogenic score is
|
|
5
|
+
-1.1, of parts +0.4 and -1.5: it does not cancel. If the residue emitted 1.5 kg, the net would
|
|
6
|
+
be 0 with parts of 1.5 each: a reader who sees 0 should know it is +1.5 - 1.5.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
from headwater.calc.arrays import MatrixLike, Vector, as_matrix
|
|
14
|
+
|
|
15
|
+
CANCELS_BELOW = 1e-3
|
|
16
|
+
"""A score cancels when it is smaller than this share of its gross, the sum of the sizes of its
|
|
17
|
+
parts. The share docs/appendix/benchmarks.md checks on ecoinvent, under "Numerics on ecoinvent"."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class Cancellation:
|
|
22
|
+
"""The positive and the negative part of a score: the sums of its positive and of its
|
|
23
|
+
negative terms, one term per flow and process."""
|
|
24
|
+
|
|
25
|
+
positive: float
|
|
26
|
+
negative: float
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def net(self) -> float:
|
|
30
|
+
"""The score: positive + negative."""
|
|
31
|
+
return self.positive + self.negative
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def gross(self) -> float:
|
|
35
|
+
"""The sum of the sizes of the parts: positive - negative."""
|
|
36
|
+
return self.positive - self.negative
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def cancels(self) -> bool:
|
|
40
|
+
"""True when the parts are not 0 and the score is less than `CANCELS_BELOW` of their
|
|
41
|
+
size. A score with no parts, an exact zero, does not cancel."""
|
|
42
|
+
return self.gross > 0 and abs(self.net) < CANCELS_BELOW * self.gross
|
|
43
|
+
|
|
44
|
+
def __add__(self, other: "Cancellation") -> "Cancellation":
|
|
45
|
+
return Cancellation(self.positive + other.positive, self.negative + other.negative)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def cancellation(B: MatrixLike, c: Vector, s: Vector) -> Cancellation:
|
|
49
|
+
"""The parts of the score `c · B s`, term by term: `c[f] × B[f, p] × s[p]` for every flow f
|
|
50
|
+
and process p.
|
|
51
|
+
|
|
52
|
+
`B` has one row per elementary flow and one column per process, `c` holds the factors of one
|
|
53
|
+
indicator and `s` how much each process runs. Terms are taken per flow, not per process, so
|
|
54
|
+
a process that takes CO2 from the air and emits it again has two parts, not a net of 0.
|
|
55
|
+
"""
|
|
56
|
+
plus, minus = process_parts(B, c)
|
|
57
|
+
return cancellation_of(plus, minus, s)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def process_parts(B: MatrixLike, c: Vector) -> tuple[Vector, Vector]:
|
|
61
|
+
"""For each process, the sum of its positive terms `c[f] × B[f, p]` and of its negative
|
|
62
|
+
ones, per unit it runs. They depend on `B` and `c` only, so a caller can keep them for every
|
|
63
|
+
`s`."""
|
|
64
|
+
matrix = as_matrix(B, "B", copy=False)
|
|
65
|
+
n_flows = matrix.shape[0]
|
|
66
|
+
if c.shape != (n_flows,):
|
|
67
|
+
raise ValueError(f"c must have one factor per flow of B ({n_flows} flows), got {c.shape}")
|
|
68
|
+
terms = matrix.multiply(c.reshape(-1, 1)).tocsc()
|
|
69
|
+
plus = np.asarray(terms.maximum(0).sum(axis=0), dtype=np.float64).ravel()
|
|
70
|
+
minus = np.asarray(terms.minimum(0).sum(axis=0), dtype=np.float64).ravel()
|
|
71
|
+
return plus, minus
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def cancellation_of(plus: Vector, minus: Vector, s: Vector) -> Cancellation:
|
|
75
|
+
"""The parts of a score from `process_parts` and how much each process runs. A term is
|
|
76
|
+
positive when its process's term and its run have the same sign, so a process that runs
|
|
77
|
+
backwards turns its terms around."""
|
|
78
|
+
if s.shape != plus.shape:
|
|
79
|
+
raise ValueError(
|
|
80
|
+
f"s must have one entry per process ({plus.shape[0]} processes), got {s.shape}"
|
|
81
|
+
)
|
|
82
|
+
up, down = np.maximum(s, 0.0), np.minimum(s, 0.0)
|
|
83
|
+
return Cancellation(
|
|
84
|
+
positive=float(plus @ up + minus @ down), negative=float(minus @ up + plus @ down)
|
|
85
|
+
)
|
headwater/calc/errors.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Errors raised when a calculation cannot give a trustworthy number."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
|
|
5
|
+
from headwater.model.errors import HeadwaterError
|
|
6
|
+
|
|
7
|
+
SHOWN = 20
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CalculationError(HeadwaterError):
|
|
11
|
+
"""A calculation cannot give a trustworthy number.
|
|
12
|
+
|
|
13
|
+
`indices` names the processes involved, as matrix indices of this calculation. The caller
|
|
14
|
+
maps them back to ids.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, message: str, indices: Iterable[int] = ()) -> None:
|
|
18
|
+
self.message = message
|
|
19
|
+
self.indices: tuple[int, ...] = tuple(int(i) for i in indices)
|
|
20
|
+
text = message
|
|
21
|
+
if self.indices:
|
|
22
|
+
shown = ", ".join(str(i) for i in self.indices[:SHOWN])
|
|
23
|
+
more = len(self.indices) - SHOWN
|
|
24
|
+
text = f"{message}: {shown}" + (f" and {more} more" if more > 0 else "")
|
|
25
|
+
super().__init__(text)
|
|
26
|
+
|
|
27
|
+
def __reduce__(self) -> tuple[type, tuple[object, ...]]:
|
|
28
|
+
return (type(self), (self.message, self.indices))
|
|
29
|
+
|
|
30
|
+
def shifted(self, offset: int) -> "CalculationError":
|
|
31
|
+
"""The same error with every index moved by `offset`.
|
|
32
|
+
|
|
33
|
+
For a calculation on a block that sits below another one in a stacked system.
|
|
34
|
+
"""
|
|
35
|
+
return type(self)(self.message, (i + offset for i in self.indices))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SingularMatrixError(CalculationError):
|
|
39
|
+
"""The technosphere matrix cannot be solved."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ResidualError(CalculationError):
|
|
43
|
+
"""A result does not satisfy its equations within the tolerance."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, message: str, indices: Iterable[int], residual: float) -> None:
|
|
46
|
+
super().__init__(f"{message} (largest {residual:.3g})", indices)
|
|
47
|
+
self.message = message
|
|
48
|
+
self.residual = residual
|
|
49
|
+
|
|
50
|
+
def __reduce__(self) -> tuple[type, tuple[object, ...]]:
|
|
51
|
+
return (type(self), (self.message, self.indices, self.residual))
|
|
52
|
+
|
|
53
|
+
def shifted(self, offset: int) -> "ResidualError":
|
|
54
|
+
return ResidualError(self.message, (i + offset for i in self.indices), self.residual)
|