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.
Files changed (207) hide show
  1. headwater/__init__.py +107 -0
  2. headwater/_lazy.py +33 -0
  3. headwater/calc/__init__.py +76 -0
  4. headwater/calc/arrays.py +45 -0
  5. headwater/calc/cancellation.py +85 -0
  6. headwater/calc/errors.py +54 -0
  7. headwater/calc/explain.py +351 -0
  8. headwater/calc/monte_carlo.py +581 -0
  9. headwater/calc/signs.py +99 -0
  10. headwater/calc/skus.py +42 -0
  11. headwater/calc/solve.py +498 -0
  12. headwater/calc/study.py +101 -0
  13. headwater/calc/update.py +378 -0
  14. headwater/calc/weighting.py +56 -0
  15. headwater/cli/__init__.py +151 -0
  16. headwater/cli/_common.py +528 -0
  17. headwater/cli/brightway.py +96 -0
  18. headwater/cli/demo.py +38 -0
  19. headwater/cli/diff.py +270 -0
  20. headwater/cli/excel.py +154 -0
  21. headwater/cli/find.py +97 -0
  22. headwater/cli/method.py +211 -0
  23. headwater/cli/release.py +155 -0
  24. headwater/cli/schema.py +35 -0
  25. headwater/cli/sensitivity.py +101 -0
  26. headwater/cli/study.py +979 -0
  27. headwater/cli/upgrade.py +316 -0
  28. headwater/cli/uses.py +112 -0
  29. headwater/cli/where.py +40 -0
  30. headwater/demo/README.md +25 -0
  31. headwater/demo/releases/toy-background@6864b7dbb65d/exchanges.parquet +0 -0
  32. headwater/demo/releases/toy-background@6864b7dbb65d/flow_properties.parquet +0 -0
  33. headwater/demo/releases/toy-background@6864b7dbb65d/flows.parquet +0 -0
  34. headwater/demo/releases/toy-background@6864b7dbb65d/geographies.parquet +0 -0
  35. headwater/demo/releases/toy-background@6864b7dbb65d/issues.parquet +0 -0
  36. headwater/demo/releases/toy-background@6864b7dbb65d/methods/toy-climate/factors.parquet +0 -0
  37. headwater/demo/releases/toy-background@6864b7dbb65d/processes.parquet +0 -0
  38. headwater/demo/releases/toy-background@6864b7dbb65d/release.json +75 -0
  39. headwater/demo/releases/toy-background@6864b7dbb65d/units.parquet +0 -0
  40. headwater/demo/studies/dyes/database.yaml +3 -0
  41. headwater/demo/studies/dyes/flows.csv +2 -0
  42. headwater/demo/studies/dyes/parameters.yaml +5 -0
  43. headwater/demo/studies/dyes/processes/reactive-dye.csv +4 -0
  44. headwater/demo/studies/dyes/processes/reactive-dye.yaml +3 -0
  45. headwater/demo/studies/tshirt/database.yaml +3 -0
  46. headwater/demo/studies/tshirt/flows.csv +2 -0
  47. headwater/demo/studies/tshirt/processes/tshirt.csv +4 -0
  48. headwater/demo/studies/tshirt/processes/tshirt.yaml +3 -0
  49. headwater/demo/studies/tshirt-dyed/database.yaml +4 -0
  50. headwater/demo/studies/tshirt-dyed/flows.csv +2 -0
  51. headwater/demo/studies/tshirt-dyed/processes/tshirt.csv +5 -0
  52. headwater/demo/studies/tshirt-dyed/processes/tshirt.yaml +3 -0
  53. headwater/demo/studies/tshirt-footprint/scenarios/cleaner-dye.yaml +5 -0
  54. headwater/demo/studies/tshirt-footprint/study.yaml +8 -0
  55. headwater/demo/studies/tshirt-local-electricity/database.yaml +3 -0
  56. headwater/demo/studies/tshirt-local-electricity/flows.csv +2 -0
  57. headwater/demo/studies/tshirt-local-electricity/overrides/own-wind-power.csv +2 -0
  58. headwater/demo/studies/tshirt-local-electricity/processes/tshirt.csv +4 -0
  59. headwater/demo/studies/tshirt-local-electricity/processes/tshirt.yaml +3 -0
  60. headwater/diff/__init__.py +51 -0
  61. headwater/diff/_attribute.py +663 -0
  62. headwater/diff/_changes.py +1127 -0
  63. headwater/diff/_git.py +389 -0
  64. headwater/diff/_moved.py +44 -0
  65. headwater/diff/_releases.py +284 -0
  66. headwater/diff/_render.py +564 -0
  67. headwater/diff/_study.py +283 -0
  68. headwater/io/__init__.py +6 -0
  69. headwater/io/allocate.py +327 -0
  70. headwater/io/brightway/__init__.py +20 -0
  71. headwater/io/brightway/importer.py +134 -0
  72. headwater/io/brightway/project.py +465 -0
  73. headwater/io/brightway/reader.py +282 -0
  74. headwater/io/ecospold1/__init__.py +87 -0
  75. headwater/io/ecospold1/assemble.py +282 -0
  76. headwater/io/ecospold1/dataset.py +403 -0
  77. headwater/io/ecospold1/reader.py +162 -0
  78. headwater/io/ecospold1/roles.py +165 -0
  79. headwater/io/ecospold1/uncertainty.py +180 -0
  80. headwater/io/ecospold1/units.py +46 -0
  81. headwater/io/ecospold2/__init__.py +57 -0
  82. headwater/io/ecospold2/dataset.py +715 -0
  83. headwater/io/ecospold2/geographies.py +59 -0
  84. headwater/io/ecospold2/masterdata.py +324 -0
  85. headwater/io/ecospold2/reader.py +382 -0
  86. headwater/io/excel/__init__.py +52 -0
  87. headwater/io/excel/activities.py +187 -0
  88. headwater/io/excel/convert.py +279 -0
  89. headwater/io/excel/database_yaml.py +217 -0
  90. headwater/io/excel/importer.py +219 -0
  91. headwater/io/excel/index.py +152 -0
  92. headwater/io/excel/lines.py +480 -0
  93. headwater/io/excel/linking.py +322 -0
  94. headwater/io/excel/mapping.py +198 -0
  95. headwater/io/excel/parameters.py +164 -0
  96. headwater/io/excel/records.py +172 -0
  97. headwater/io/excel/report.py +257 -0
  98. headwater/io/excel/uncertainty.py +145 -0
  99. headwater/io/excel/units.py +89 -0
  100. headwater/io/excel/workbook.py +524 -0
  101. headwater/io/excel/write.py +224 -0
  102. headwater/io/exclude.py +105 -0
  103. headwater/io/files.py +25 -0
  104. headwater/io/formats.py +122 -0
  105. headwater/io/inventory.py +269 -0
  106. headwater/io/lcia/__init__.py +49 -0
  107. headwater/io/lcia/reader.py +138 -0
  108. headwater/io/lcia/workbook.py +413 -0
  109. headwater/io/method.py +412 -0
  110. headwater/io/nwset.py +109 -0
  111. headwater/io/olca_jsonld/__init__.py +26 -0
  112. headwater/io/olca_jsonld/flows.py +194 -0
  113. headwater/io/olca_jsonld/inventory.py +413 -0
  114. headwater/io/olca_jsonld/method.py +201 -0
  115. headwater/io/olca_jsonld/package.py +185 -0
  116. headwater/io/simapro_csv/__init__.py +43 -0
  117. headwater/io/simapro_csv/blocks.py +146 -0
  118. headwater/io/simapro_csv/export.py +162 -0
  119. headwater/io/simapro_csv/header.py +111 -0
  120. headwater/io/simapro_csv/method.py +129 -0
  121. headwater/io/simapro_csv/reader.py +297 -0
  122. headwater/io/simapro_csv/records.py +573 -0
  123. headwater/io/simapro_csv/text.py +40 -0
  124. headwater/io/simapro_csv/units.py +40 -0
  125. headwater/io/source.py +279 -0
  126. headwater/io/units.py +199 -0
  127. headwater/link/__init__.py +42 -0
  128. headwater/link/by_id.py +92 -0
  129. headwater/link/by_name.py +248 -0
  130. headwater/link/flows.py +113 -0
  131. headwater/link/lines.py +192 -0
  132. headwater/link/mapping.py +241 -0
  133. headwater/model/__init__.py +112 -0
  134. headwater/model/_types.py +17 -0
  135. headwater/model/catalog.py +155 -0
  136. headwater/model/database.py +117 -0
  137. headwater/model/errors.py +122 -0
  138. headwater/model/flows.py +63 -0
  139. headwater/model/formula.py +561 -0
  140. headwater/model/issue.py +81 -0
  141. headwater/model/method.py +224 -0
  142. headwater/model/nwset.py +217 -0
  143. headwater/model/override.py +106 -0
  144. headwater/model/parameters.py +320 -0
  145. headwater/model/process.py +232 -0
  146. headwater/model/roles.py +36 -0
  147. headwater/model/scenario.py +147 -0
  148. headwater/model/study.py +71 -0
  149. headwater/model/uncertainty.py +112 -0
  150. headwater/model/units.py +134 -0
  151. headwater/py.typed +0 -0
  152. headwater/query/__init__.py +80 -0
  153. headwater/query/_git.py +28 -0
  154. headwater/query/_hash.py +66 -0
  155. headwater/query/_internals.py +29 -0
  156. headwater/query/_open.py +27 -0
  157. headwater/query/_release.py +488 -0
  158. headwater/query/_scenario.py +248 -0
  159. headwater/query/_study_draws.py +482 -0
  160. headwater/query/_system.py +418 -0
  161. headwater/query/_uncertainty.py +175 -0
  162. headwater/query/background.py +1019 -0
  163. headwater/query/database.py +866 -0
  164. headwater/query/monte_carlo.py +617 -0
  165. headwater/query/result.py +1133 -0
  166. headwater/query/search.py +335 -0
  167. headwater/query/skus.py +279 -0
  168. headwater/query/study.py +607 -0
  169. headwater/release/__init__.py +51 -0
  170. headwater/release/cache.py +186 -0
  171. headwater/release/checks.py +1016 -0
  172. headwater/release/content.py +245 -0
  173. headwater/release/indicators.py +162 -0
  174. headwater/release/manifest.py +103 -0
  175. headwater/release/read.py +380 -0
  176. headwater/release/remote.py +159 -0
  177. headwater/release/schema.py +408 -0
  178. headwater/release/write.py +518 -0
  179. headwater/study/__init__.py +44 -0
  180. headwater/study/_cut_offs.py +264 -0
  181. headwater/study/_exchanges.py +207 -0
  182. headwater/study/_files.py +324 -0
  183. headwater/study/_no_factor.py +107 -0
  184. headwater/study/_overrides.py +117 -0
  185. headwater/study/_parameters.py +206 -0
  186. headwater/study/_scenarios.py +480 -0
  187. headwater/study/_study.py +308 -0
  188. headwater/study/_uncertainty.py +118 -0
  189. headwater/study/_uses.py +199 -0
  190. headwater/study/_write.py +361 -0
  191. headwater/study/edit.py +934 -0
  192. headwater/study/folders.py +168 -0
  193. headwater/study/reader.py +625 -0
  194. headwater/study/schema.py +171 -0
  195. headwater/study/validate.py +686 -0
  196. headwater/upgrade/__init__.py +30 -0
  197. headwater/upgrade/_datasets.py +133 -0
  198. headwater/upgrade/_flags.py +132 -0
  199. headwater/upgrade/_moves.py +103 -0
  200. headwater/upgrade/_plan.py +466 -0
  201. headwater/upgrade/_rules.py +214 -0
  202. headwater/upgrade/_write.py +213 -0
  203. headwater-0.1.0.dist-info/METADATA +150 -0
  204. headwater-0.1.0.dist-info/RECORD +207 -0
  205. headwater-0.1.0.dist-info/WHEEL +4 -0
  206. headwater-0.1.0.dist-info/entry_points.txt +3 -0
  207. 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
+ ]
@@ -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
+ )
@@ -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)