ecosemantic-engine 0.1.8__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.
- ecosemantic_engine-0.1.8/.gitignore +24 -0
- ecosemantic_engine-0.1.8/LICENSE +73 -0
- ecosemantic_engine-0.1.8/NOTICE +13 -0
- ecosemantic_engine-0.1.8/PKG-INFO +52 -0
- ecosemantic_engine-0.1.8/README.md +21 -0
- ecosemantic_engine-0.1.8/ecosemantic/engine/__init__.py +0 -0
- ecosemantic_engine-0.1.8/ecosemantic/engine/background.py +345 -0
- ecosemantic_engine-0.1.8/ecosemantic/engine/calculation.py +293 -0
- ecosemantic_engine-0.1.8/ecosemantic/engine/foreground.py +320 -0
- ecosemantic_engine-0.1.8/ecosemantic/engine/loops.py +39 -0
- ecosemantic_engine-0.1.8/ecosemantic/engine/montecarlo.py +350 -0
- ecosemantic_engine-0.1.8/ecosemantic/engine/paths.py +189 -0
- ecosemantic_engine-0.1.8/ecosemantic/engine/py.typed +0 -0
- ecosemantic_engine-0.1.8/ecosemantic/engine/refine.py +38 -0
- ecosemantic_engine-0.1.8/ecosemantic/engine/solver.py +90 -0
- ecosemantic_engine-0.1.8/ecosemantic/errors/__init__.py +205 -0
- ecosemantic_engine-0.1.8/ecosemantic/errors/py.typed +0 -0
- ecosemantic_engine-0.1.8/ecosemantic/model/__init__.py +0 -0
- ecosemantic_engine-0.1.8/ecosemantic/model/canonical.py +119 -0
- ecosemantic_engine-0.1.8/ecosemantic/model/entities.py +475 -0
- ecosemantic_engine-0.1.8/ecosemantic/model/py.typed +0 -0
- ecosemantic_engine-0.1.8/ecosemantic/model/refs.py +137 -0
- ecosemantic_engine-0.1.8/ecosemantic/model/summary.py +53 -0
- ecosemantic_engine-0.1.8/ecosemantic/model/units.py +181 -0
- ecosemantic_engine-0.1.8/ecosemantic/model/validation.py +520 -0
- ecosemantic_engine-0.1.8/pyproject.toml +45 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
.venv/
|
|
4
|
+
venv/
|
|
5
|
+
*.egg-info/
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
node_modules/
|
|
9
|
+
.pytest_cache/
|
|
10
|
+
.ruff_cache/
|
|
11
|
+
.mypy_cache/
|
|
12
|
+
.coverage
|
|
13
|
+
coverage.json
|
|
14
|
+
htmlcov/
|
|
15
|
+
|
|
16
|
+
.env
|
|
17
|
+
.env.*
|
|
18
|
+
!.env.example
|
|
19
|
+
|
|
20
|
+
.DS_Store
|
|
21
|
+
graphify-out
|
|
22
|
+
knowledge/
|
|
23
|
+
.okf-cache.json
|
|
24
|
+
**/ecosemantic/view/static/
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# PolyForm Noncommercial License 1.0.0
|
|
2
|
+
|
|
3
|
+
<https://polyformproject.org/licenses/noncommercial/1.0.0>
|
|
4
|
+
|
|
5
|
+
## Acceptance
|
|
6
|
+
|
|
7
|
+
In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses.
|
|
8
|
+
|
|
9
|
+
## Copyright License
|
|
10
|
+
|
|
11
|
+
The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only distribute the software according to [Distribution License](#distribution-license) and make changes or new works based on the software according to [Changes and New Works License](#changes-and-new-works-license).
|
|
12
|
+
|
|
13
|
+
## Distribution License
|
|
14
|
+
|
|
15
|
+
The licensor grants you an additional copyright license to distribute copies of the software. Your license to distribute covers distributing the software with changes and new works permitted by [Changes and New Works License](#changes-and-new-works-license).
|
|
16
|
+
|
|
17
|
+
## Notices
|
|
18
|
+
|
|
19
|
+
You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. For example:
|
|
20
|
+
|
|
21
|
+
> Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
|
|
22
|
+
|
|
23
|
+
## Changes and New Works License
|
|
24
|
+
|
|
25
|
+
The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose.
|
|
26
|
+
|
|
27
|
+
## Patent License
|
|
28
|
+
|
|
29
|
+
The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software.
|
|
30
|
+
|
|
31
|
+
## Noncommercial Purposes
|
|
32
|
+
|
|
33
|
+
Any noncommercial purpose is a permitted purpose.
|
|
34
|
+
|
|
35
|
+
## Personal Uses
|
|
36
|
+
|
|
37
|
+
Personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, without any anticipated commercial application, is use for a permitted purpose.
|
|
38
|
+
|
|
39
|
+
## Noncommercial Organizations
|
|
40
|
+
|
|
41
|
+
Use by any charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization, or government institution is use for a permitted purpose regardless of the source of funding or obligations resulting from the funding.
|
|
42
|
+
|
|
43
|
+
## Fair Use
|
|
44
|
+
|
|
45
|
+
You may have "fair use" rights for the software under the law. These terms do not limit them.
|
|
46
|
+
|
|
47
|
+
## No Other Rights
|
|
48
|
+
|
|
49
|
+
These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses.
|
|
50
|
+
|
|
51
|
+
## Patent Defense
|
|
52
|
+
|
|
53
|
+
If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
|
|
54
|
+
|
|
55
|
+
## Violations
|
|
56
|
+
|
|
57
|
+
The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately.
|
|
58
|
+
|
|
59
|
+
## No Liability
|
|
60
|
+
|
|
61
|
+
***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.***
|
|
62
|
+
|
|
63
|
+
## Definitions
|
|
64
|
+
|
|
65
|
+
The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms.
|
|
66
|
+
|
|
67
|
+
**You** refers to the individual or entity agreeing to these terms.
|
|
68
|
+
|
|
69
|
+
**Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. **Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
|
|
70
|
+
|
|
71
|
+
**Your licenses** are all the licenses granted to you for the software under these terms.
|
|
72
|
+
|
|
73
|
+
**Use** means anything you do with the software requiring one of your licenses.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
EcoSemantic
|
|
2
|
+
Required Notice: Copyright 2026 Carlos Gaete-Morales (https://github.com/cdgaete/ecosemantic)
|
|
3
|
+
|
|
4
|
+
EcoSemantic is licensed under the PolyForm Noncommercial License 1.0.0, in LICENSE
|
|
5
|
+
and at https://polyformproject.org/licenses/noncommercial/1.0.0. Any noncommercial
|
|
6
|
+
purpose is permitted. Commercial use needs a separate licence from Carlos
|
|
7
|
+
Gaete-Morales: cdgaete@gmail.com.
|
|
8
|
+
|
|
9
|
+
The page that ecosemantic-view carries bundles third-party software under its
|
|
10
|
+
own licences; their notices are in ecosemantic/view/static/THIRD_PARTY_NOTICES.txt
|
|
11
|
+
inside that distribution. Components of the page adapt shadcn/ui, Copyright (c)
|
|
12
|
+
2023 shadcn, under the MIT licence, as page/PROVENANCE.md in the source
|
|
13
|
+
repository records.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ecosemantic-engine
|
|
3
|
+
Version: 0.1.8
|
|
4
|
+
Summary: The EcoSemantic product-system vocabulary, its typed errors, and the exact LCA computation.
|
|
5
|
+
Project-URL: Homepage, https://github.com/cdgaete/ecosemantic
|
|
6
|
+
Project-URL: Repository, https://github.com/cdgaete/ecosemantic
|
|
7
|
+
Project-URL: Issues, https://github.com/cdgaete/ecosemantic/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/cdgaete/ecosemantic/blob/main/CHANGELOG.md
|
|
9
|
+
Author-email: Carlos Gaete-Morales <cdgaete@gmail.com>
|
|
10
|
+
License-Expression: PolyForm-Noncommercial-1.0.0
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
License-File: NOTICE
|
|
13
|
+
Keywords: LCA,Monte Carlo,environmental impact,life cycle assessment,sparse linear algebra
|
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.12
|
|
24
|
+
Requires-Dist: nimblend>=0.20261012.0
|
|
25
|
+
Requires-Dist: numpy>=2.5
|
|
26
|
+
Requires-Dist: pydantic<3,>=2.7
|
|
27
|
+
Requires-Dist: scipy>=1.16
|
|
28
|
+
Provides-Extra: mkl
|
|
29
|
+
Requires-Dist: pypardiso>=0.4.6; ((sys_platform == 'linux' and platform_machine == 'x86_64') or (sys_platform == 'win32' and platform_machine == 'AMD64')) and extra == 'mkl'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# ecosemantic-engine
|
|
33
|
+
|
|
34
|
+
The lowest distribution of the EcoSemantic core: the product-system vocabulary, the typed errors every other distribution raises, and the exact life cycle assessment computation over a compiled background.
|
|
35
|
+
|
|
36
|
+
## Names it publishes
|
|
37
|
+
|
|
38
|
+
- `ecosemantic.model` — the vocabulary a product system is made of: its entities, the refs that name what it reaches, the units the core knows, the structural validation a system passes before a calculation runs, the result-relevant projection a version hash names, and the summary an answer keeps of itself.
|
|
39
|
+
- `ecosemantic.errors` — the typed errors, `EcoSemanticError` and the refusals under it: a layer that cannot answer raises rather than approximates.
|
|
40
|
+
- `ecosemantic.engine` — the computation: `background` compiles a database into labelled memory-mapped arrays, `solver` makes the forward and adjoint solves from one factorisation, `calculation` reads scores, contributions, intensities, supply, inventory and paths from one solve, `foreground` gives a product system its columns over that background, `paths` walks the supply chain under one process, `montecarlo` takes each draw from the seed and the draw's index, `loops` judges the technosphere loops a draw is checked on, and `refine` refines a sparse solve.
|
|
41
|
+
|
|
42
|
+
## What it depends on
|
|
43
|
+
|
|
44
|
+
pydantic, numpy and scipy, and nimblend for the memory-mapped arrays a background is compiled into. The `mkl` extra adds pypardiso, on Linux x86-64 and Windows x86-64, where MKL publishes wheels.
|
|
45
|
+
|
|
46
|
+
## Installing it
|
|
47
|
+
|
|
48
|
+
`pip install ecosemantic` installs this distribution with the other five and the `ecosemantic` command. `pip install ecosemantic-engine` installs it alone, for code that builds on the vocabulary and the engine without a server; `pip install "ecosemantic-engine[mkl]"` adds the solver.
|
|
49
|
+
|
|
50
|
+
## Licence
|
|
51
|
+
|
|
52
|
+
PolyForm Noncommercial License 1.0.0, copyright Carlos Gaete-Morales: `LICENSE` and `NOTICE` in this distribution. Any noncommercial purpose is permitted, by individuals and by charities, schools, public research and government bodies; commercial use needs a separate licence from him (cdgaete@gmail.com). EcoSemantic is his trademark, and the licence grants no right to use that name (`TRADEMARK.md` in the repository). The code is published from https://github.com/cdgaete/ecosemantic.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# ecosemantic-engine
|
|
2
|
+
|
|
3
|
+
The lowest distribution of the EcoSemantic core: the product-system vocabulary, the typed errors every other distribution raises, and the exact life cycle assessment computation over a compiled background.
|
|
4
|
+
|
|
5
|
+
## Names it publishes
|
|
6
|
+
|
|
7
|
+
- `ecosemantic.model` — the vocabulary a product system is made of: its entities, the refs that name what it reaches, the units the core knows, the structural validation a system passes before a calculation runs, the result-relevant projection a version hash names, and the summary an answer keeps of itself.
|
|
8
|
+
- `ecosemantic.errors` — the typed errors, `EcoSemanticError` and the refusals under it: a layer that cannot answer raises rather than approximates.
|
|
9
|
+
- `ecosemantic.engine` — the computation: `background` compiles a database into labelled memory-mapped arrays, `solver` makes the forward and adjoint solves from one factorisation, `calculation` reads scores, contributions, intensities, supply, inventory and paths from one solve, `foreground` gives a product system its columns over that background, `paths` walks the supply chain under one process, `montecarlo` takes each draw from the seed and the draw's index, `loops` judges the technosphere loops a draw is checked on, and `refine` refines a sparse solve.
|
|
10
|
+
|
|
11
|
+
## What it depends on
|
|
12
|
+
|
|
13
|
+
pydantic, numpy and scipy, and nimblend for the memory-mapped arrays a background is compiled into. The `mkl` extra adds pypardiso, on Linux x86-64 and Windows x86-64, where MKL publishes wheels.
|
|
14
|
+
|
|
15
|
+
## Installing it
|
|
16
|
+
|
|
17
|
+
`pip install ecosemantic` installs this distribution with the other five and the `ecosemantic` command. `pip install ecosemantic-engine` installs it alone, for code that builds on the vocabulary and the engine without a server; `pip install "ecosemantic-engine[mkl]"` adds the solver.
|
|
18
|
+
|
|
19
|
+
## Licence
|
|
20
|
+
|
|
21
|
+
PolyForm Noncommercial License 1.0.0, copyright Carlos Gaete-Morales: `LICENSE` and `NOTICE` in this distribution. Any noncommercial purpose is permitted, by individuals and by charities, schools, public research and government bodies; commercial use needs a separate licence from him (cdgaete@gmail.com). EcoSemantic is his trademark, and the licence grants no right to use that name (`TRADEMARK.md` in the repository). The code is published from https://github.com/cdgaete/ecosemantic.
|
|
File without changes
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
"""A database compiled for calculation: its matrices as labelled arrays, its exchange rows with
|
|
2
|
+
their distributions, and the directory of numpy files it is saved as."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
from dataclasses import dataclass, fields
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import nimblend as nb
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
from ecosemantic.errors import Conflict, Invalid, NotFound
|
|
16
|
+
from ecosemantic.model.entities import DISTRIBUTION_IDS, Method, Uncertainty
|
|
17
|
+
from ecosemantic.model.refs import Database
|
|
18
|
+
|
|
19
|
+
FORMAT = 1
|
|
20
|
+
_PARAMETERS = ("loc", "scale", "minimum", "maximum")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class Rows:
|
|
25
|
+
"""Exchange rows by label, as a source states them: each row's row and column label, its
|
|
26
|
+
amount, whether it enters the matrix negated, and its distribution."""
|
|
27
|
+
|
|
28
|
+
rows: Sequence[str]
|
|
29
|
+
columns: Sequence[str]
|
|
30
|
+
amounts: Sequence[float]
|
|
31
|
+
flipped: Sequence[bool]
|
|
32
|
+
uncertainties: Sequence[Uncertainty | None]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class Factors:
|
|
37
|
+
"""Characterisation factors by label: each one's category, its flow and its value."""
|
|
38
|
+
|
|
39
|
+
categories: Sequence[str]
|
|
40
|
+
flows: Sequence[str]
|
|
41
|
+
values: Sequence[float]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class Exchanges:
|
|
46
|
+
"""Exchange rows unsummed, at the positions of a background's coordinates: each row's row and
|
|
47
|
+
column, its amount as stated, whether it enters the matrix negated, and its distribution:
|
|
48
|
+
the type as `DISTRIBUTION_IDS` numbers it, 0 for none, and NaN for a parameter the type has
|
|
49
|
+
not."""
|
|
50
|
+
|
|
51
|
+
row: np.ndarray
|
|
52
|
+
column: np.ndarray
|
|
53
|
+
amount: np.ndarray
|
|
54
|
+
flip: np.ndarray
|
|
55
|
+
uncertainty_type: np.ndarray
|
|
56
|
+
loc: np.ndarray
|
|
57
|
+
scale: np.ndarray
|
|
58
|
+
minimum: np.ndarray
|
|
59
|
+
maximum: np.ndarray
|
|
60
|
+
negative: np.ndarray
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def of(
|
|
64
|
+
cls,
|
|
65
|
+
row: np.ndarray,
|
|
66
|
+
column: np.ndarray,
|
|
67
|
+
amounts: Sequence[float],
|
|
68
|
+
flipped: Sequence[bool],
|
|
69
|
+
uncertainties: Sequence[Uncertainty | None],
|
|
70
|
+
) -> Exchanges:
|
|
71
|
+
"""Rows at positions, with their distributions as columns."""
|
|
72
|
+
n = len(amounts)
|
|
73
|
+
kind = np.zeros(n, dtype=np.int8)
|
|
74
|
+
parameters = {name: np.full(n, np.nan) for name in _PARAMETERS}
|
|
75
|
+
negative = np.zeros(n, dtype=bool)
|
|
76
|
+
for at, uncertainty in enumerate(uncertainties):
|
|
77
|
+
if uncertainty is None:
|
|
78
|
+
continue
|
|
79
|
+
kind[at] = DISTRIBUTION_IDS[uncertainty.type]
|
|
80
|
+
for name, value in uncertainty.params.items():
|
|
81
|
+
if name == "negative":
|
|
82
|
+
negative[at] = bool(value)
|
|
83
|
+
else:
|
|
84
|
+
parameters[name][at] = float(value)
|
|
85
|
+
return cls(
|
|
86
|
+
row=np.asarray(row, dtype=np.int32),
|
|
87
|
+
column=np.asarray(column, dtype=np.int32),
|
|
88
|
+
amount=np.asarray(amounts, dtype=np.float64),
|
|
89
|
+
flip=np.asarray(flipped, dtype=bool),
|
|
90
|
+
uncertainty_type=kind,
|
|
91
|
+
negative=negative,
|
|
92
|
+
**parameters,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _known(what: str, labels: np.ndarray, coordinate: np.ndarray) -> None:
|
|
97
|
+
"""Invalid when a row names a label the coordinate does not hold."""
|
|
98
|
+
unknown = labels[~np.isin(labels, coordinate)]
|
|
99
|
+
if unknown.size:
|
|
100
|
+
raise Invalid(
|
|
101
|
+
f"{unknown.size} {what} name a label the background does not hold, "
|
|
102
|
+
f"the first {str(unknown[0])!r}"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _exchanges(rows: Rows, row: nb.StoredCoord, column: nb.StoredCoord) -> Exchanges:
|
|
107
|
+
"""The rows at the positions of two coordinates, their distributions as columns."""
|
|
108
|
+
return Exchanges.of(
|
|
109
|
+
row=row.to_position(np.asarray(rows.rows)),
|
|
110
|
+
column=column.to_position(np.asarray(rows.columns)),
|
|
111
|
+
amounts=rows.amounts,
|
|
112
|
+
flipped=rows.flipped,
|
|
113
|
+
uncertainties=rows.uncertainties,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _signed(rows: Rows) -> np.ndarray:
|
|
118
|
+
"""Each row's amount as it enters the matrix."""
|
|
119
|
+
return np.where(np.asarray(rows.flipped, dtype=bool), -1.0, 1.0) * np.asarray(
|
|
120
|
+
rows.amounts, dtype=np.float64
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class Background:
|
|
125
|
+
"""A database compiled for calculation. `technosphere` is over ("product", "activity"),
|
|
126
|
+
`biosphere` over ("flow", "activity") and `characterization` over ("category", "flow").
|
|
127
|
+
"product" and "activity" share one coordinate: one label per dataset, its native id."""
|
|
128
|
+
|
|
129
|
+
def __init__(
|
|
130
|
+
self,
|
|
131
|
+
database: Database,
|
|
132
|
+
methods: Sequence[Method],
|
|
133
|
+
coords: dict[str, nb.StoredCoord],
|
|
134
|
+
technosphere: nb.SparseArray,
|
|
135
|
+
biosphere: nb.SparseArray,
|
|
136
|
+
characterization: nb.SparseArray,
|
|
137
|
+
exchanges: dict[str, Exchanges],
|
|
138
|
+
) -> None:
|
|
139
|
+
self.database = database
|
|
140
|
+
self.methods = tuple(methods)
|
|
141
|
+
self.coords = coords
|
|
142
|
+
self.technosphere = technosphere
|
|
143
|
+
self.biosphere = biosphere
|
|
144
|
+
self.characterization = characterization
|
|
145
|
+
self.exchanges = exchanges
|
|
146
|
+
self._category = {method.id: at for at, method in enumerate(self.methods)}
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
def compile(
|
|
150
|
+
cls,
|
|
151
|
+
database: Database,
|
|
152
|
+
activities: Sequence[str],
|
|
153
|
+
flows: Sequence[str],
|
|
154
|
+
methods: Sequence[Method],
|
|
155
|
+
technosphere: Rows,
|
|
156
|
+
biosphere: Rows,
|
|
157
|
+
factors: Factors,
|
|
158
|
+
) -> Background:
|
|
159
|
+
"""The background of these datasets, flows and methods. A dataset's rows at one
|
|
160
|
+
coordinate, its production and its use of its own product, are added. Invalid for a row
|
|
161
|
+
naming a label outside its coordinate."""
|
|
162
|
+
activity_labels = np.array(sorted(activities))
|
|
163
|
+
flow_labels = np.array(sorted(flows))
|
|
164
|
+
category_labels = np.array([method.id for method in methods])
|
|
165
|
+
_known("technosphere rows", np.asarray(technosphere.rows), activity_labels)
|
|
166
|
+
_known("technosphere columns", np.asarray(technosphere.columns), activity_labels)
|
|
167
|
+
_known("biosphere rows", np.asarray(biosphere.rows), flow_labels)
|
|
168
|
+
_known("biosphere columns", np.asarray(biosphere.columns), activity_labels)
|
|
169
|
+
_known("factor categories", np.asarray(factors.categories), category_labels)
|
|
170
|
+
_known("factor flows", np.asarray(factors.flows), flow_labels)
|
|
171
|
+
activity = nb.StoredCoord(activity_labels)
|
|
172
|
+
coords = {
|
|
173
|
+
"product": activity,
|
|
174
|
+
"activity": activity,
|
|
175
|
+
"flow": nb.StoredCoord(flow_labels),
|
|
176
|
+
"category": nb.StoredCoord(category_labels),
|
|
177
|
+
}
|
|
178
|
+
return cls(
|
|
179
|
+
database,
|
|
180
|
+
methods,
|
|
181
|
+
coords,
|
|
182
|
+
nb.from_long(
|
|
183
|
+
("product", "activity"),
|
|
184
|
+
coords,
|
|
185
|
+
{
|
|
186
|
+
"product": np.asarray(technosphere.rows),
|
|
187
|
+
"activity": np.asarray(technosphere.columns),
|
|
188
|
+
},
|
|
189
|
+
_signed(technosphere),
|
|
190
|
+
on_duplicate="sum",
|
|
191
|
+
),
|
|
192
|
+
nb.from_long(
|
|
193
|
+
("flow", "activity"),
|
|
194
|
+
coords,
|
|
195
|
+
{"flow": np.asarray(biosphere.rows), "activity": np.asarray(biosphere.columns)},
|
|
196
|
+
_signed(biosphere),
|
|
197
|
+
on_duplicate="sum",
|
|
198
|
+
),
|
|
199
|
+
nb.from_long(
|
|
200
|
+
("category", "flow"),
|
|
201
|
+
coords,
|
|
202
|
+
{"category": np.asarray(factors.categories), "flow": np.asarray(factors.flows)},
|
|
203
|
+
np.asarray(factors.values, dtype=np.float64),
|
|
204
|
+
on_duplicate="sum",
|
|
205
|
+
),
|
|
206
|
+
{
|
|
207
|
+
"technosphere": _exchanges(technosphere, activity, activity),
|
|
208
|
+
"biosphere": _exchanges(biosphere, coords["flow"], activity),
|
|
209
|
+
},
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def treating(self) -> frozenset[str]:
|
|
213
|
+
"""The datasets whose production, net of their use of their own product, enters the
|
|
214
|
+
matrix negative: the treatments, each taking in the waste its reference names, so a
|
|
215
|
+
consumer sends it a negative amount. ecoinvent states such a reference as a negative
|
|
216
|
+
output and openLCA as an input; both compile to a negative production."""
|
|
217
|
+
rows = self.exchanges["technosphere"]
|
|
218
|
+
own = np.asarray(rows.row) == np.asarray(rows.column)
|
|
219
|
+
signed = np.where(np.asarray(rows.flip)[own], -1.0, 1.0) * np.asarray(rows.amount)[own]
|
|
220
|
+
labels = self.coords["activity"].labels
|
|
221
|
+
net = np.bincount(np.asarray(rows.column)[own], weights=signed, minlength=labels.size)
|
|
222
|
+
return frozenset(str(label) for label in labels[net < 0])
|
|
223
|
+
|
|
224
|
+
def position(self, dim: str, label: str) -> int | None:
|
|
225
|
+
"""The position of a label along "activity" or "flow", whose labels are sorted, or
|
|
226
|
+
None."""
|
|
227
|
+
labels = self.coords[dim].labels
|
|
228
|
+
at = int(np.searchsorted(labels, label))
|
|
229
|
+
return at if at < labels.size and labels[at] == label else None
|
|
230
|
+
|
|
231
|
+
def positions(self, methods: Sequence[Method]) -> list[int]:
|
|
232
|
+
"""The position of each method's category, in the order given; NotFound naming every
|
|
233
|
+
method the background does not hold."""
|
|
234
|
+
missing = [method.spec for method in methods if method.id not in self._category]
|
|
235
|
+
if missing:
|
|
236
|
+
raise NotFound(f"{self.database.name!r} lacks methods {missing}")
|
|
237
|
+
return [self._category[method.id] for method in methods]
|
|
238
|
+
|
|
239
|
+
def arrays(self) -> dict[str, np.ndarray]:
|
|
240
|
+
"""Every array the background is saved as, by the name of its file: the label columns,
|
|
241
|
+
each matrix's index and data, and each exchange column."""
|
|
242
|
+
arrays: dict[str, np.ndarray] = {
|
|
243
|
+
"activities": np.asarray(self.coords["activity"].labels),
|
|
244
|
+
"flows": np.asarray(self.coords["flow"].labels),
|
|
245
|
+
"categories": np.asarray(self.coords["category"].labels),
|
|
246
|
+
}
|
|
247
|
+
for name in ("technosphere", "biosphere", "characterization"):
|
|
248
|
+
array: nb.SparseArray = getattr(self, name)
|
|
249
|
+
arrays[f"{name}.index"] = np.asarray(array.index)
|
|
250
|
+
arrays[f"{name}.data"] = np.asarray(array.data)
|
|
251
|
+
for matrix, exchanges in self.exchanges.items():
|
|
252
|
+
for field in fields(Exchanges):
|
|
253
|
+
arrays[f"{matrix}.exchanges.{field.name}"] = np.asarray(
|
|
254
|
+
getattr(exchanges, field.name)
|
|
255
|
+
)
|
|
256
|
+
return arrays
|
|
257
|
+
|
|
258
|
+
def fingerprint(self) -> str:
|
|
259
|
+
"""SHA-256 of the arrays in the order of their names, each as its name, its dtype, its
|
|
260
|
+
shape and its bytes in C order, the category labels taken as each category's key less
|
|
261
|
+
the database name, since a method id carries the name its database is installed under:
|
|
262
|
+
what a calculation reads, apart from that name, background.json's other fields and the
|
|
263
|
+
files' headers, so one build installed under any name, and every build holding the same
|
|
264
|
+
numbers, shares it."""
|
|
265
|
+
arrays = self.arrays()
|
|
266
|
+
arrays["categories"] = np.asarray(
|
|
267
|
+
[" > ".join(method.tuple[1:]) for method in self.methods], dtype=np.str_
|
|
268
|
+
)
|
|
269
|
+
digest = hashlib.sha256()
|
|
270
|
+
for name, array in sorted(arrays.items()):
|
|
271
|
+
values = np.ascontiguousarray(array)
|
|
272
|
+
digest.update(f"{name}\0{values.dtype.str}\0{values.shape}\0".encode())
|
|
273
|
+
digest.update(values.tobytes())
|
|
274
|
+
return digest.hexdigest()
|
|
275
|
+
|
|
276
|
+
def save(self, directory: Path) -> None:
|
|
277
|
+
"""Write the background into a new directory: one .npy file per array and label column,
|
|
278
|
+
and background.json, written last, with the format, the database, the methods and the
|
|
279
|
+
counts."""
|
|
280
|
+
if directory.exists():
|
|
281
|
+
raise Conflict(f"{directory} exists; save the background to a new directory")
|
|
282
|
+
directory.mkdir(parents=True)
|
|
283
|
+
arrays = self.arrays()
|
|
284
|
+
for name, values in arrays.items():
|
|
285
|
+
np.save(directory / f"{name}.npy", values, allow_pickle=False)
|
|
286
|
+
manifest = {
|
|
287
|
+
"format": FORMAT,
|
|
288
|
+
"database": self.database.model_dump(mode="json"),
|
|
289
|
+
"methods": [method.model_dump(mode="json") for method in self.methods],
|
|
290
|
+
"counts": {name: int(values.shape[-1]) for name, values in arrays.items()},
|
|
291
|
+
}
|
|
292
|
+
(directory / "background.json").write_text(
|
|
293
|
+
json.dumps(manifest, indent=1) + "\n", encoding="utf-8"
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
@classmethod
|
|
297
|
+
def load(cls, directory: Path) -> Background:
|
|
298
|
+
"""The background saved in a directory, its arrays memory-mapped. NotFound for a
|
|
299
|
+
directory holding none, Invalid for a format this engine does not read."""
|
|
300
|
+
manifest_path = directory / "background.json"
|
|
301
|
+
if not manifest_path.is_file():
|
|
302
|
+
raise NotFound(f"{directory} holds no saved background")
|
|
303
|
+
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
304
|
+
if manifest.get("format") != FORMAT:
|
|
305
|
+
raise Invalid(
|
|
306
|
+
f"{directory} holds background format {manifest.get('format')!r}; "
|
|
307
|
+
f"this engine reads format {FORMAT}"
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
def read(name: str) -> np.ndarray:
|
|
311
|
+
return np.load(directory / f"{name}.npy", mmap_mode="r", allow_pickle=False)
|
|
312
|
+
|
|
313
|
+
activity = nb.StoredCoord(read("activities"))
|
|
314
|
+
coords = {
|
|
315
|
+
"product": activity,
|
|
316
|
+
"activity": activity,
|
|
317
|
+
"flow": nb.StoredCoord(read("flows")),
|
|
318
|
+
"category": nb.StoredCoord(read("categories")),
|
|
319
|
+
}
|
|
320
|
+
dims = {
|
|
321
|
+
"technosphere": ("product", "activity"),
|
|
322
|
+
"biosphere": ("flow", "activity"),
|
|
323
|
+
"characterization": ("category", "flow"),
|
|
324
|
+
}
|
|
325
|
+
arrays = {
|
|
326
|
+
name: nb.SparseArray.from_canonical(
|
|
327
|
+
read(f"{name}.index"), read(f"{name}.data"), coords, frame
|
|
328
|
+
)
|
|
329
|
+
for name, frame in dims.items()
|
|
330
|
+
}
|
|
331
|
+
exchanges = {
|
|
332
|
+
matrix: Exchanges(
|
|
333
|
+
**{f.name: read(f"{matrix}.exchanges.{f.name}") for f in fields(Exchanges)}
|
|
334
|
+
)
|
|
335
|
+
for matrix in ("technosphere", "biosphere")
|
|
336
|
+
}
|
|
337
|
+
return cls(
|
|
338
|
+
Database.model_validate(manifest["database"]),
|
|
339
|
+
[Method.model_validate(method) for method in manifest["methods"]],
|
|
340
|
+
coords,
|
|
341
|
+
arrays["technosphere"],
|
|
342
|
+
arrays["biosphere"],
|
|
343
|
+
arrays["characterization"],
|
|
344
|
+
exchanges,
|
|
345
|
+
)
|