bw-processing 1.2__tar.gz → 1.4__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.
- {bw_processing-1.2/src/bw_processing.egg-info → bw_processing-1.4}/PKG-INFO +199 -2
- {bw_processing-1.2 → bw_processing-1.4}/README.md +197 -1
- {bw_processing-1.2 → bw_processing-1.4}/pyproject.toml +1 -0
- {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/__init__.py +15 -2
- {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/array_creation.py +29 -65
- {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/constants.py +7 -4
- {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/datapackage.py +505 -15
- {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/io_helpers.py +35 -0
- {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/matrix_entry.py +67 -0
- bw_processing-1.4/src/bw_processing/param_labels.py +100 -0
- {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/proxies.py +8 -0
- {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/unique_fields.py +17 -0
- {bw_processing-1.2 → bw_processing-1.4/src/bw_processing.egg-info}/PKG-INFO +199 -2
- {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing.egg-info/SOURCES.txt +2 -0
- {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing.egg-info/requires.txt +1 -0
- bw_processing-1.4/tests/test_array_creation.py +97 -0
- {bw_processing-1.2 → bw_processing-1.4}/tests/test_datapackage.py +352 -1
- {bw_processing-1.2 → bw_processing-1.4}/tests/test_matrix_entry.py +119 -1
- bw_processing-1.4/tests/test_params.py +483 -0
- bw_processing-1.2/tests/test_array_creation.py +0 -14
- {bw_processing-1.2 → bw_processing-1.4}/LICENSE +0 -0
- {bw_processing-1.2 → bw_processing-1.4}/MANIFEST.in +0 -0
- {bw_processing-1.2 → bw_processing-1.4}/setup.cfg +0 -0
- {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/errors.py +0 -0
- {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/examples/__init__.py +0 -0
- {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/examples/datapackage_1/another name.indices.parquet +0 -0
- {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/examples/datapackage_1/sa-data-vector-from-dict.data.parquet +0 -0
- {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/examples/datapackage_1/sa-data-vector-from-dict.distributions.parquet +0 -0
- {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/examples/datapackage_1/sa-data-vector-from-dict.flip.parquet +0 -0
- {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/examples/datapackage_1/sa-data-vector-from-dict.indices.parquet +0 -0
- {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/examples/datapackage_1/some name.data.parquet +0 -0
- {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/examples/datapackage_1/some name.flip.parquet +0 -0
- {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/examples/datapackage_1/some name.indices.parquet +0 -0
- {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/examples/datapackage_2.zip +0 -0
- {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/examples/interfaces.py +0 -0
- {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/examples/parquet_files.py +0 -0
- {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/examples/simple.zip +0 -0
- {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/filesystem.py +0 -0
- {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/indexing.py +0 -0
- {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/io_parquet_helpers.py +0 -0
- {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/io_pyarrow_helpers.py +0 -0
- {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/merging.py +0 -0
- {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/utils.py +0 -0
- {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing.egg-info/dependency_links.txt +0 -0
- {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing.egg-info/top_level.txt +0 -0
- {bw_processing-1.2 → bw_processing-1.4}/tests/test_filesystem.py +0 -0
- {bw_processing-1.2 → bw_processing-1.4}/tests/test_filtered_datapackage.py +0 -0
- {bw_processing-1.2 → bw_processing-1.4}/tests/test_indexing.py +0 -0
- {bw_processing-1.2 → bw_processing-1.4}/tests/test_integration.py +0 -0
- {bw_processing-1.2 → bw_processing-1.4}/tests/test_interfaces.py +0 -0
- {bw_processing-1.2 → bw_processing-1.4}/tests/test_io_parquet_helpers.py +0 -0
- {bw_processing-1.2 → bw_processing-1.4}/tests/test_io_pyarrow_helpers.py +0 -0
- {bw_processing-1.2 → bw_processing-1.4}/tests/test_loading.py +0 -0
- {bw_processing-1.2 → bw_processing-1.4}/tests/test_merging.py +0 -0
- {bw_processing-1.2 → bw_processing-1.4}/tests/test_proxies.py +0 -0
- {bw_processing-1.2 → bw_processing-1.4}/tests/test_unique_fields.py +0 -0
- {bw_processing-1.2 → bw_processing-1.4}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bw_processing
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4
|
|
4
4
|
Summary: Foo
|
|
5
5
|
Author-email: Chris Mutel <cmutel@gmail.com>
|
|
6
6
|
Maintainer-email: Chris Mutel <cmutel@gmail.com>
|
|
@@ -21,6 +21,7 @@ Requires-Python: >=3.9
|
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
License-File: LICENSE
|
|
23
23
|
Requires-Dist: fsspec
|
|
24
|
+
Requires-Dist: jsonschema>=4.0
|
|
24
25
|
Requires-Dist: morefs
|
|
25
26
|
Requires-Dist: numpy
|
|
26
27
|
Requires-Dist: pandas
|
|
@@ -67,6 +68,13 @@ Library for storing numeric data for use in matrix-based calculations. Designed
|
|
|
67
68
|
|
|
68
69
|
- [Background](#background)
|
|
69
70
|
- [Concepts](#concepts)
|
|
71
|
+
- [Data packages](#data-packages)
|
|
72
|
+
- [Vectors versus arrays](#vectors-versus-arrays)
|
|
73
|
+
- [Persistent versus dynamic](#persistent-versus-dynamic)
|
|
74
|
+
- [Scale arrays](#scale-arrays)
|
|
75
|
+
- [Parameter arrays for sensitivity analysis](#parameter-arrays-for-sensitivity-analysis)
|
|
76
|
+
- [NaN as a sentinel value](#nan-as-a-sentinel-value)
|
|
77
|
+
- [Policies](#policies)
|
|
70
78
|
- [Install](#install)
|
|
71
79
|
- [Usage](#usage)
|
|
72
80
|
- [Contributing](#contributing)
|
|
@@ -84,6 +92,8 @@ The [Brightway LCA framework](https://brightway.dev/) has stored data used in co
|
|
|
84
92
|
* **Dynamic data sources**. Instead of requiring that data for matrix construction be present and savedd on disk, it can now be generated dynamically, either through code running locally or on another computer system. This is a big step towards embeddding life cycle assessment in a web of environmental models.
|
|
85
93
|
* **Use [fsspec](https://filesystem-spec.readthedocs.io/en/latest/) for file IO**. The use of this library allows for data packages to be stored on your local computer, or on [many logical or virtual file systems](https://docs.pyfilesystem.org/en/latest/guide.html).
|
|
86
94
|
* **Simpler handling of numeric values whose sign should be flipped**. Sometimes it is more convenient to specify positive numbers in dataset definitions, even though such numbers should be negative when inserted into the resulting matrices. For example, in the technosphere matrix in life cycle assessment, products produced are positive and products consumed are negative, though both values are given as positive in datasets. Brightway used to use a type mapping dictionary to indicate which values in a matrix should have their sign flipped after insertion. Such mapping dictionaries are brittle and inelegant. `bw_processing` uses an optional boolean vector, called `flip`, to indicate if any values should be flipped.
|
|
95
|
+
* **Per-exchange multiplicative scaling**. An optional float vector, called `scale`, can be attached to any resource group. Each element is a multiplicative factor applied to the corresponding data value — whether static or sampled stochastically — before it is inserted into the matrix. Typical uses are allocation factors and unit conversions. A value of `1.0` leaves the data unchanged.
|
|
96
|
+
* **Recording independent variables for sensitivity analysis**. An optional `params_array` can be attached to any resource group to record the values of model parameters (independent variables) that were used to generate the data. For array resources each column of `params_array` corresponds to the same column in `data_array`, making it straightforward to correlate inputs with outputs for sensitivity analysis methods such as Morris or Sobol.
|
|
87
97
|
* **Separation of uncertainty distribution parameters from other data**. Fitting data to a [probability density function](https://en.wikipedia.org/wiki/Probability_density_function) (PDF), or an estimate of such a PDF, is only one approach to quantitative uncertainty analysis. We would like to support other approaches, including [direct sampling from real data](https://github.com/PascalLesage/presamples/). Therefore, uncertainty distribution parameters are stored separately, only loaded if needed, and are only one way to express quantitative uncertainty.
|
|
88
98
|
|
|
89
99
|
## Concepts
|
|
@@ -96,6 +106,18 @@ Data objects can be vectors or arrays. Vectors will always produce the same matr
|
|
|
96
106
|
|
|
97
107
|
### Vectors versus arrays
|
|
98
108
|
|
|
109
|
+
Vectors and arrays differ in how many possible values they provide per matrix cell.
|
|
110
|
+
|
|
111
|
+
A **vector** provides one value per cell. Every time a vector resource is used, it produces the same result. This is the standard case for deterministic LCA calculations.
|
|
112
|
+
|
|
113
|
+
An **array** provides multiple possible values per cell, stored as columns of a 2-D numpy array. Each time the data package is iterated, a different column is selected and inserted into the matrix. This is used for:
|
|
114
|
+
|
|
115
|
+
* **Monte Carlo analysis** — columns hold independently sampled values drawn from the uncertainty distributions.
|
|
116
|
+
* **Scenario analysis** — each column represents a predefined scenario, such as a different technology mix or policy assumption.
|
|
117
|
+
* **Presamples** — a generalization of the [presamples library](https://github.com/PascalLesage/presamples/), where pre-drawn samples are stored for reproducibility.
|
|
118
|
+
|
|
119
|
+
Which column is selected on each iteration is controlled by the `sequential` and `combinatorial` policies; see [Policies](#policies).
|
|
120
|
+
|
|
99
121
|
### Persistent versus dynamic
|
|
100
122
|
|
|
101
123
|
Persistent data is fixed, and can be completely loaded into memory and used directly or written to disk. Dynamic data is only resolved as the data is used, during matrix construction and iteration. Dynamic data is provided by *interfaces* - Python code that either generates the data, or wraps data coming from other software. There are many possible use cases for data interfaces, including:
|
|
@@ -187,6 +209,181 @@ print(data_obj.url)
|
|
|
187
209
|
>>> "example.com"
|
|
188
210
|
```
|
|
189
211
|
|
|
212
|
+
### Scale arrays
|
|
213
|
+
|
|
214
|
+
Any resource group (persistent or dynamic, vector or array) can carry an optional `scale_array`: a one-dimensional float array of the same length as `indices_array`. Each element is a multiplicative factor applied to the corresponding data value before it is inserted into the matrix. The factor is applied to both static and stochastically-sampled values. A value of `1.0` leaves the data unchanged.
|
|
215
|
+
|
|
216
|
+
Typical use cases:
|
|
217
|
+
|
|
218
|
+
* **Allocation factors** — when a process produces multiple products, the exchange amounts must be partitioned between them. Storing the allocation coefficients as a `scale_array` keeps them alongside the data they modify without requiring a separate processing step.
|
|
219
|
+
* **Unit conversions** — when source data is expressed in a unit that differs from the matrix convention, a constant conversion factor can be stored as a `scale_array` rather than baked into every data value.
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
import numpy as np
|
|
223
|
+
from bw_processing import create_datapackage
|
|
224
|
+
from bw_processing.constants import INDICES_DTYPE
|
|
225
|
+
|
|
226
|
+
dp = create_datapackage()
|
|
227
|
+
indices_array = np.array([(1, 4), (2, 5), (3, 6)], dtype=INDICES_DTYPE)
|
|
228
|
+
data_array = np.array([100.0, 200.0, 300.0])
|
|
229
|
+
scale_array = np.array([0.6, 1.0, 0.4]) # e.g. allocation factors
|
|
230
|
+
|
|
231
|
+
dp.add_persistent_vector(
|
|
232
|
+
matrix="technosphere",
|
|
233
|
+
name="my-process",
|
|
234
|
+
indices_array=indices_array,
|
|
235
|
+
data_array=data_array,
|
|
236
|
+
scale_array=scale_array,
|
|
237
|
+
)
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
The stored resource has `kind="scale"` and can be retrieved via `dp.get_resource("my-process.scale")`. The `scale_array` must be a float dtype (`float32` or `float64`); passing an integer array raises `WrongDatatype`.
|
|
241
|
+
|
|
242
|
+
### Parameter arrays for sensitivity analysis
|
|
243
|
+
|
|
244
|
+
Any resource group can carry an optional `params_array` that records the values of the independent variables (model parameters) used to generate the data. This is the foundation for global sensitivity analysis workflows such as [Morris screening](https://en.wikipedia.org/wiki/Morris_method) or [Sobol indices](https://en.wikipedia.org/wiki/Variance-based_sensitivity_analysis).
|
|
245
|
+
|
|
246
|
+
**Shape conventions:**
|
|
247
|
+
|
|
248
|
+
| Resource type | `data_array` shape | `params_array` shape |
|
|
249
|
+
|---|---|---|
|
|
250
|
+
| persistent / dynamic vector | `(n_exchanges,)` | `(n_params,)` |
|
|
251
|
+
| persistent array | `(n_exchanges, n_scenarios)` | `(n_params, n_scenarios)` |
|
|
252
|
+
| dynamic array | `(n_exchanges, n_scenarios)` | `(n_params, n_scenarios)` — column count not validated against interface |
|
|
253
|
+
|
|
254
|
+
Column `j` of `params_array` describes the parameter configuration that produced column `j` of `data_array`. The `params_array` must be a float dtype.
|
|
255
|
+
|
|
256
|
+
#### Basic usage
|
|
257
|
+
|
|
258
|
+
```python
|
|
259
|
+
import numpy as np
|
|
260
|
+
from bw_processing import create_datapackage, INDICES_DTYPE
|
|
261
|
+
|
|
262
|
+
dp = create_datapackage()
|
|
263
|
+
indices = np.array([(1, 4), (2, 5)], dtype=INDICES_DTYPE)
|
|
264
|
+
|
|
265
|
+
# Store the parameter values alongside the data
|
|
266
|
+
dp.add_persistent_vector(
|
|
267
|
+
matrix="technosphere",
|
|
268
|
+
name="my-process",
|
|
269
|
+
indices_array=indices,
|
|
270
|
+
data_array=np.array([100.0, 200.0]),
|
|
271
|
+
params_array=np.array([25.0, 1.013]), # temperature (°C), pressure (atm)
|
|
272
|
+
)
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
#### Adding labels
|
|
276
|
+
|
|
277
|
+
`param_labels` is an optional list of label objects (strings or dicts) whose length must match `params_array.shape[0]`. When provided, a companion `name.param_labels.json` file is written inside the same resource group, containing a `"values"` list and an optional `"schema"` (a [JSON Schema](https://json-schema.org/) document). Pass `param_label_schema=StringLabelSchema()` for plain-string labels, or a `ParamLabelSchema` for structured dict labels.
|
|
278
|
+
|
|
279
|
+
```python
|
|
280
|
+
import numpy as np
|
|
281
|
+
from bw_processing import create_datapackage, INDICES_DTYPE, StringLabelSchema
|
|
282
|
+
|
|
283
|
+
dp = create_datapackage()
|
|
284
|
+
dp.add_persistent_vector(
|
|
285
|
+
matrix="technosphere",
|
|
286
|
+
name="my-process",
|
|
287
|
+
indices_array=np.array([(1, 4)], dtype=INDICES_DTYPE),
|
|
288
|
+
data_array=np.array([100.0]),
|
|
289
|
+
params_array=np.array([25.0, 1.013]),
|
|
290
|
+
param_labels=["temperature", "pressure"],
|
|
291
|
+
param_label_schema=StringLabelSchema(),
|
|
292
|
+
)
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
#### Structured labels with a schema
|
|
296
|
+
|
|
297
|
+
Use `ParamLabelSchema` and `ParamLabelField` when labels are structured objects, and pass a `param_label_schema` to validate each label at write time:
|
|
298
|
+
|
|
299
|
+
```python
|
|
300
|
+
import numpy as np
|
|
301
|
+
from bw_processing import (
|
|
302
|
+
create_datapackage,
|
|
303
|
+
INDICES_DTYPE,
|
|
304
|
+
ParamLabelField,
|
|
305
|
+
ParamLabelSchema,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
dp = create_datapackage()
|
|
309
|
+
|
|
310
|
+
schema = ParamLabelSchema(
|
|
311
|
+
fields=[
|
|
312
|
+
ParamLabelField(name="name", type="string"),
|
|
313
|
+
ParamLabelField(name="database", type="string"),
|
|
314
|
+
ParamLabelField(name="year", type="integer", required=False),
|
|
315
|
+
],
|
|
316
|
+
description="Brightway activity reference",
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
param_labels = [
|
|
320
|
+
{"name": "electricity", "database": "ecoinvent", "year": 2020},
|
|
321
|
+
{"name": "heat", "database": "ecoinvent"},
|
|
322
|
+
]
|
|
323
|
+
|
|
324
|
+
dp.add_persistent_array(
|
|
325
|
+
matrix="technosphere",
|
|
326
|
+
name="sa-run",
|
|
327
|
+
indices_array=np.array([(1, 4), (2, 5)], dtype=INDICES_DTYPE),
|
|
328
|
+
data_array=np.array([[10.0, 20.0, 30.0], # 2 exchanges × 3 scenarios
|
|
329
|
+
[40.0, 50.0, 60.0]]),
|
|
330
|
+
params_array=np.array([[25.0, 30.0, 35.0], # temperature: 2 params × 3 scenarios
|
|
331
|
+
[1.0, 1.1, 1.2]]),
|
|
332
|
+
param_labels=param_labels,
|
|
333
|
+
param_label_schema=schema, # validates every label against the JSON Schema on write
|
|
334
|
+
)
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
`ParamLabelField.type` accepts the standard JSON Schema type names: `"string"`, `"integer"`, `"number"`, `"boolean"`. For plain-string labels you can also pass `param_label_schema=StringLabelSchema()` to make the schema explicit.
|
|
338
|
+
|
|
339
|
+
#### Retrieving params and labels
|
|
340
|
+
|
|
341
|
+
```python
|
|
342
|
+
params_data, _ = dp.get_resource("sa-run.params") # numpy array
|
|
343
|
+
labels_data, _ = dp.get_resource("sa-run.param_labels") # {"schema": ..., "values": [...]}
|
|
344
|
+
|
|
345
|
+
# Reconstruct the schema dataclass from the stored JSON Schema:
|
|
346
|
+
from bw_processing import schema_from_json_schema
|
|
347
|
+
schema = schema_from_json_schema(labels_data["schema"])
|
|
348
|
+
# → ParamLabelSchema(fields=[...])
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
#### Dependency
|
|
352
|
+
|
|
353
|
+
`params_array` validation uses the [`jsonschema`](https://python-jsonschema.readthedocs.io/) library, which is a required dependency of `bw_processing`.
|
|
354
|
+
|
|
355
|
+
### NaN as a sentinel value
|
|
356
|
+
|
|
357
|
+
A `NaN` value in a data vector or array is treated by `matrix_utils` as **"no data insertion"** — that element is skipped when the matrix is built or rebuilt, leaving the corresponding matrix cell at whatever value was written by an earlier package. This convention makes it straightforward to define scenario or override packages: set an element to `NaN` to inherit the base value, or to a real number to override it.
|
|
358
|
+
|
|
359
|
+
```python
|
|
360
|
+
import numpy as np
|
|
361
|
+
from bw_processing import create_datapackage, INDICES_DTYPE
|
|
362
|
+
|
|
363
|
+
# Base package — sets matrix[0,0]=5 and matrix[1,1]=7
|
|
364
|
+
dp_base = create_datapackage()
|
|
365
|
+
dp_base.add_persistent_vector(
|
|
366
|
+
matrix="foo",
|
|
367
|
+
name="base",
|
|
368
|
+
indices_array=np.array([(0, 0), (1, 1)], dtype=INDICES_DTYPE),
|
|
369
|
+
data_array=np.array([5.0, 7.0]),
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
# Scenario package — overrides matrix[1,1] but leaves matrix[0,0] untouched
|
|
373
|
+
dp_scenario = create_datapackage()
|
|
374
|
+
dp_scenario.add_persistent_vector(
|
|
375
|
+
matrix="foo",
|
|
376
|
+
name="scenario",
|
|
377
|
+
indices_array=np.array([(0, 0), (1, 1)], dtype=INDICES_DTYPE),
|
|
378
|
+
data_array=np.array([np.nan, 99.0]),
|
|
379
|
+
)
|
|
380
|
+
# When both packages are passed to MappedMatrix:
|
|
381
|
+
# matrix[0,0] == 5.0 (NaN in scenario → base value preserved)
|
|
382
|
+
# matrix[1,1] == 99.0 (non-NaN in scenario → override applied)
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Note that `NaN` skipping is implemented in `matrix_utils`, not in `bw_processing` itself. `bw_processing` stores and retrieves the `NaN` values faithfully; the skip logic runs at matrix construction time.
|
|
386
|
+
|
|
190
387
|
### Policies
|
|
191
388
|
|
|
192
389
|
Data package policies define how the data should be used. Policies apply to the entire data package; you may wish to adjust what is stored in which data packages to get the effect you desire.
|
|
@@ -219,7 +416,7 @@ Please make sure you understand how `combinatorial` and `sequential` interact! T
|
|
|
219
416
|
|
|
220
417
|
## Install
|
|
221
418
|
|
|
222
|
-
Install using pip or conda (channel `
|
|
419
|
+
Install using pip or conda (channel `conda-forge`). Depends on `numpy` and `pandas` (for reading and writing CSVs).
|
|
223
420
|
|
|
224
421
|
Has no explicit or implicit dependence on any other part of Brightway.
|
|
225
422
|
|
|
@@ -25,6 +25,13 @@ Library for storing numeric data for use in matrix-based calculations. Designed
|
|
|
25
25
|
|
|
26
26
|
- [Background](#background)
|
|
27
27
|
- [Concepts](#concepts)
|
|
28
|
+
- [Data packages](#data-packages)
|
|
29
|
+
- [Vectors versus arrays](#vectors-versus-arrays)
|
|
30
|
+
- [Persistent versus dynamic](#persistent-versus-dynamic)
|
|
31
|
+
- [Scale arrays](#scale-arrays)
|
|
32
|
+
- [Parameter arrays for sensitivity analysis](#parameter-arrays-for-sensitivity-analysis)
|
|
33
|
+
- [NaN as a sentinel value](#nan-as-a-sentinel-value)
|
|
34
|
+
- [Policies](#policies)
|
|
28
35
|
- [Install](#install)
|
|
29
36
|
- [Usage](#usage)
|
|
30
37
|
- [Contributing](#contributing)
|
|
@@ -42,6 +49,8 @@ The [Brightway LCA framework](https://brightway.dev/) has stored data used in co
|
|
|
42
49
|
* **Dynamic data sources**. Instead of requiring that data for matrix construction be present and savedd on disk, it can now be generated dynamically, either through code running locally or on another computer system. This is a big step towards embeddding life cycle assessment in a web of environmental models.
|
|
43
50
|
* **Use [fsspec](https://filesystem-spec.readthedocs.io/en/latest/) for file IO**. The use of this library allows for data packages to be stored on your local computer, or on [many logical or virtual file systems](https://docs.pyfilesystem.org/en/latest/guide.html).
|
|
44
51
|
* **Simpler handling of numeric values whose sign should be flipped**. Sometimes it is more convenient to specify positive numbers in dataset definitions, even though such numbers should be negative when inserted into the resulting matrices. For example, in the technosphere matrix in life cycle assessment, products produced are positive and products consumed are negative, though both values are given as positive in datasets. Brightway used to use a type mapping dictionary to indicate which values in a matrix should have their sign flipped after insertion. Such mapping dictionaries are brittle and inelegant. `bw_processing` uses an optional boolean vector, called `flip`, to indicate if any values should be flipped.
|
|
52
|
+
* **Per-exchange multiplicative scaling**. An optional float vector, called `scale`, can be attached to any resource group. Each element is a multiplicative factor applied to the corresponding data value — whether static or sampled stochastically — before it is inserted into the matrix. Typical uses are allocation factors and unit conversions. A value of `1.0` leaves the data unchanged.
|
|
53
|
+
* **Recording independent variables for sensitivity analysis**. An optional `params_array` can be attached to any resource group to record the values of model parameters (independent variables) that were used to generate the data. For array resources each column of `params_array` corresponds to the same column in `data_array`, making it straightforward to correlate inputs with outputs for sensitivity analysis methods such as Morris or Sobol.
|
|
45
54
|
* **Separation of uncertainty distribution parameters from other data**. Fitting data to a [probability density function](https://en.wikipedia.org/wiki/Probability_density_function) (PDF), or an estimate of such a PDF, is only one approach to quantitative uncertainty analysis. We would like to support other approaches, including [direct sampling from real data](https://github.com/PascalLesage/presamples/). Therefore, uncertainty distribution parameters are stored separately, only loaded if needed, and are only one way to express quantitative uncertainty.
|
|
46
55
|
|
|
47
56
|
## Concepts
|
|
@@ -54,6 +63,18 @@ Data objects can be vectors or arrays. Vectors will always produce the same matr
|
|
|
54
63
|
|
|
55
64
|
### Vectors versus arrays
|
|
56
65
|
|
|
66
|
+
Vectors and arrays differ in how many possible values they provide per matrix cell.
|
|
67
|
+
|
|
68
|
+
A **vector** provides one value per cell. Every time a vector resource is used, it produces the same result. This is the standard case for deterministic LCA calculations.
|
|
69
|
+
|
|
70
|
+
An **array** provides multiple possible values per cell, stored as columns of a 2-D numpy array. Each time the data package is iterated, a different column is selected and inserted into the matrix. This is used for:
|
|
71
|
+
|
|
72
|
+
* **Monte Carlo analysis** — columns hold independently sampled values drawn from the uncertainty distributions.
|
|
73
|
+
* **Scenario analysis** — each column represents a predefined scenario, such as a different technology mix or policy assumption.
|
|
74
|
+
* **Presamples** — a generalization of the [presamples library](https://github.com/PascalLesage/presamples/), where pre-drawn samples are stored for reproducibility.
|
|
75
|
+
|
|
76
|
+
Which column is selected on each iteration is controlled by the `sequential` and `combinatorial` policies; see [Policies](#policies).
|
|
77
|
+
|
|
57
78
|
### Persistent versus dynamic
|
|
58
79
|
|
|
59
80
|
Persistent data is fixed, and can be completely loaded into memory and used directly or written to disk. Dynamic data is only resolved as the data is used, during matrix construction and iteration. Dynamic data is provided by *interfaces* - Python code that either generates the data, or wraps data coming from other software. There are many possible use cases for data interfaces, including:
|
|
@@ -145,6 +166,181 @@ print(data_obj.url)
|
|
|
145
166
|
>>> "example.com"
|
|
146
167
|
```
|
|
147
168
|
|
|
169
|
+
### Scale arrays
|
|
170
|
+
|
|
171
|
+
Any resource group (persistent or dynamic, vector or array) can carry an optional `scale_array`: a one-dimensional float array of the same length as `indices_array`. Each element is a multiplicative factor applied to the corresponding data value before it is inserted into the matrix. The factor is applied to both static and stochastically-sampled values. A value of `1.0` leaves the data unchanged.
|
|
172
|
+
|
|
173
|
+
Typical use cases:
|
|
174
|
+
|
|
175
|
+
* **Allocation factors** — when a process produces multiple products, the exchange amounts must be partitioned between them. Storing the allocation coefficients as a `scale_array` keeps them alongside the data they modify without requiring a separate processing step.
|
|
176
|
+
* **Unit conversions** — when source data is expressed in a unit that differs from the matrix convention, a constant conversion factor can be stored as a `scale_array` rather than baked into every data value.
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
import numpy as np
|
|
180
|
+
from bw_processing import create_datapackage
|
|
181
|
+
from bw_processing.constants import INDICES_DTYPE
|
|
182
|
+
|
|
183
|
+
dp = create_datapackage()
|
|
184
|
+
indices_array = np.array([(1, 4), (2, 5), (3, 6)], dtype=INDICES_DTYPE)
|
|
185
|
+
data_array = np.array([100.0, 200.0, 300.0])
|
|
186
|
+
scale_array = np.array([0.6, 1.0, 0.4]) # e.g. allocation factors
|
|
187
|
+
|
|
188
|
+
dp.add_persistent_vector(
|
|
189
|
+
matrix="technosphere",
|
|
190
|
+
name="my-process",
|
|
191
|
+
indices_array=indices_array,
|
|
192
|
+
data_array=data_array,
|
|
193
|
+
scale_array=scale_array,
|
|
194
|
+
)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
The stored resource has `kind="scale"` and can be retrieved via `dp.get_resource("my-process.scale")`. The `scale_array` must be a float dtype (`float32` or `float64`); passing an integer array raises `WrongDatatype`.
|
|
198
|
+
|
|
199
|
+
### Parameter arrays for sensitivity analysis
|
|
200
|
+
|
|
201
|
+
Any resource group can carry an optional `params_array` that records the values of the independent variables (model parameters) used to generate the data. This is the foundation for global sensitivity analysis workflows such as [Morris screening](https://en.wikipedia.org/wiki/Morris_method) or [Sobol indices](https://en.wikipedia.org/wiki/Variance-based_sensitivity_analysis).
|
|
202
|
+
|
|
203
|
+
**Shape conventions:**
|
|
204
|
+
|
|
205
|
+
| Resource type | `data_array` shape | `params_array` shape |
|
|
206
|
+
|---|---|---|
|
|
207
|
+
| persistent / dynamic vector | `(n_exchanges,)` | `(n_params,)` |
|
|
208
|
+
| persistent array | `(n_exchanges, n_scenarios)` | `(n_params, n_scenarios)` |
|
|
209
|
+
| dynamic array | `(n_exchanges, n_scenarios)` | `(n_params, n_scenarios)` — column count not validated against interface |
|
|
210
|
+
|
|
211
|
+
Column `j` of `params_array` describes the parameter configuration that produced column `j` of `data_array`. The `params_array` must be a float dtype.
|
|
212
|
+
|
|
213
|
+
#### Basic usage
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
import numpy as np
|
|
217
|
+
from bw_processing import create_datapackage, INDICES_DTYPE
|
|
218
|
+
|
|
219
|
+
dp = create_datapackage()
|
|
220
|
+
indices = np.array([(1, 4), (2, 5)], dtype=INDICES_DTYPE)
|
|
221
|
+
|
|
222
|
+
# Store the parameter values alongside the data
|
|
223
|
+
dp.add_persistent_vector(
|
|
224
|
+
matrix="technosphere",
|
|
225
|
+
name="my-process",
|
|
226
|
+
indices_array=indices,
|
|
227
|
+
data_array=np.array([100.0, 200.0]),
|
|
228
|
+
params_array=np.array([25.0, 1.013]), # temperature (°C), pressure (atm)
|
|
229
|
+
)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
#### Adding labels
|
|
233
|
+
|
|
234
|
+
`param_labels` is an optional list of label objects (strings or dicts) whose length must match `params_array.shape[0]`. When provided, a companion `name.param_labels.json` file is written inside the same resource group, containing a `"values"` list and an optional `"schema"` (a [JSON Schema](https://json-schema.org/) document). Pass `param_label_schema=StringLabelSchema()` for plain-string labels, or a `ParamLabelSchema` for structured dict labels.
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
import numpy as np
|
|
238
|
+
from bw_processing import create_datapackage, INDICES_DTYPE, StringLabelSchema
|
|
239
|
+
|
|
240
|
+
dp = create_datapackage()
|
|
241
|
+
dp.add_persistent_vector(
|
|
242
|
+
matrix="technosphere",
|
|
243
|
+
name="my-process",
|
|
244
|
+
indices_array=np.array([(1, 4)], dtype=INDICES_DTYPE),
|
|
245
|
+
data_array=np.array([100.0]),
|
|
246
|
+
params_array=np.array([25.0, 1.013]),
|
|
247
|
+
param_labels=["temperature", "pressure"],
|
|
248
|
+
param_label_schema=StringLabelSchema(),
|
|
249
|
+
)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
#### Structured labels with a schema
|
|
253
|
+
|
|
254
|
+
Use `ParamLabelSchema` and `ParamLabelField` when labels are structured objects, and pass a `param_label_schema` to validate each label at write time:
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
import numpy as np
|
|
258
|
+
from bw_processing import (
|
|
259
|
+
create_datapackage,
|
|
260
|
+
INDICES_DTYPE,
|
|
261
|
+
ParamLabelField,
|
|
262
|
+
ParamLabelSchema,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
dp = create_datapackage()
|
|
266
|
+
|
|
267
|
+
schema = ParamLabelSchema(
|
|
268
|
+
fields=[
|
|
269
|
+
ParamLabelField(name="name", type="string"),
|
|
270
|
+
ParamLabelField(name="database", type="string"),
|
|
271
|
+
ParamLabelField(name="year", type="integer", required=False),
|
|
272
|
+
],
|
|
273
|
+
description="Brightway activity reference",
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
param_labels = [
|
|
277
|
+
{"name": "electricity", "database": "ecoinvent", "year": 2020},
|
|
278
|
+
{"name": "heat", "database": "ecoinvent"},
|
|
279
|
+
]
|
|
280
|
+
|
|
281
|
+
dp.add_persistent_array(
|
|
282
|
+
matrix="technosphere",
|
|
283
|
+
name="sa-run",
|
|
284
|
+
indices_array=np.array([(1, 4), (2, 5)], dtype=INDICES_DTYPE),
|
|
285
|
+
data_array=np.array([[10.0, 20.0, 30.0], # 2 exchanges × 3 scenarios
|
|
286
|
+
[40.0, 50.0, 60.0]]),
|
|
287
|
+
params_array=np.array([[25.0, 30.0, 35.0], # temperature: 2 params × 3 scenarios
|
|
288
|
+
[1.0, 1.1, 1.2]]),
|
|
289
|
+
param_labels=param_labels,
|
|
290
|
+
param_label_schema=schema, # validates every label against the JSON Schema on write
|
|
291
|
+
)
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
`ParamLabelField.type` accepts the standard JSON Schema type names: `"string"`, `"integer"`, `"number"`, `"boolean"`. For plain-string labels you can also pass `param_label_schema=StringLabelSchema()` to make the schema explicit.
|
|
295
|
+
|
|
296
|
+
#### Retrieving params and labels
|
|
297
|
+
|
|
298
|
+
```python
|
|
299
|
+
params_data, _ = dp.get_resource("sa-run.params") # numpy array
|
|
300
|
+
labels_data, _ = dp.get_resource("sa-run.param_labels") # {"schema": ..., "values": [...]}
|
|
301
|
+
|
|
302
|
+
# Reconstruct the schema dataclass from the stored JSON Schema:
|
|
303
|
+
from bw_processing import schema_from_json_schema
|
|
304
|
+
schema = schema_from_json_schema(labels_data["schema"])
|
|
305
|
+
# → ParamLabelSchema(fields=[...])
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
#### Dependency
|
|
309
|
+
|
|
310
|
+
`params_array` validation uses the [`jsonschema`](https://python-jsonschema.readthedocs.io/) library, which is a required dependency of `bw_processing`.
|
|
311
|
+
|
|
312
|
+
### NaN as a sentinel value
|
|
313
|
+
|
|
314
|
+
A `NaN` value in a data vector or array is treated by `matrix_utils` as **"no data insertion"** — that element is skipped when the matrix is built or rebuilt, leaving the corresponding matrix cell at whatever value was written by an earlier package. This convention makes it straightforward to define scenario or override packages: set an element to `NaN` to inherit the base value, or to a real number to override it.
|
|
315
|
+
|
|
316
|
+
```python
|
|
317
|
+
import numpy as np
|
|
318
|
+
from bw_processing import create_datapackage, INDICES_DTYPE
|
|
319
|
+
|
|
320
|
+
# Base package — sets matrix[0,0]=5 and matrix[1,1]=7
|
|
321
|
+
dp_base = create_datapackage()
|
|
322
|
+
dp_base.add_persistent_vector(
|
|
323
|
+
matrix="foo",
|
|
324
|
+
name="base",
|
|
325
|
+
indices_array=np.array([(0, 0), (1, 1)], dtype=INDICES_DTYPE),
|
|
326
|
+
data_array=np.array([5.0, 7.0]),
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
# Scenario package — overrides matrix[1,1] but leaves matrix[0,0] untouched
|
|
330
|
+
dp_scenario = create_datapackage()
|
|
331
|
+
dp_scenario.add_persistent_vector(
|
|
332
|
+
matrix="foo",
|
|
333
|
+
name="scenario",
|
|
334
|
+
indices_array=np.array([(0, 0), (1, 1)], dtype=INDICES_DTYPE),
|
|
335
|
+
data_array=np.array([np.nan, 99.0]),
|
|
336
|
+
)
|
|
337
|
+
# When both packages are passed to MappedMatrix:
|
|
338
|
+
# matrix[0,0] == 5.0 (NaN in scenario → base value preserved)
|
|
339
|
+
# matrix[1,1] == 99.0 (non-NaN in scenario → override applied)
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Note that `NaN` skipping is implemented in `matrix_utils`, not in `bw_processing` itself. `bw_processing` stores and retrieves the `NaN` values faithfully; the skip logic runs at matrix construction time.
|
|
343
|
+
|
|
148
344
|
### Policies
|
|
149
345
|
|
|
150
346
|
Data package policies define how the data should be used. Policies apply to the entire data package; you may wish to adjust what is stored in which data packages to get the effect you desire.
|
|
@@ -177,7 +373,7 @@ Please make sure you understand how `combinatorial` and `sequential` interact! T
|
|
|
177
373
|
|
|
178
374
|
## Install
|
|
179
375
|
|
|
180
|
-
Install using pip or conda (channel `
|
|
376
|
+
Install using pip or conda (channel `conda-forge`). Depends on `numpy` and `pandas` (for reading and writing CSVs).
|
|
181
377
|
|
|
182
378
|
Has no explicit or implicit dependence on any other part of Brightway.
|
|
183
379
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
__all__ = (
|
|
2
2
|
"__version__",
|
|
3
|
+
"AnyLabelSchema",
|
|
3
4
|
"as_unique_attributes",
|
|
4
5
|
"as_unique_attributes_dataframe",
|
|
5
6
|
"clean_datapackage_name",
|
|
@@ -16,24 +17,36 @@ __all__ = (
|
|
|
16
17
|
"generic_zipfile_filesystem",
|
|
17
18
|
"INDICES_DTYPE",
|
|
18
19
|
"load_datapackage",
|
|
20
|
+
"ArrayEntry",
|
|
19
21
|
"MatrixEntry",
|
|
20
22
|
"MatrixName",
|
|
21
23
|
"MatrixSerializeFormat",
|
|
22
24
|
"md5",
|
|
23
25
|
"merge_datapackages_with_mask",
|
|
26
|
+
"ParamLabelField",
|
|
27
|
+
"ParamLabelSchema",
|
|
24
28
|
"reindex",
|
|
25
29
|
"reset_index",
|
|
26
30
|
"safe_filename",
|
|
31
|
+
"schema_from_json_schema",
|
|
27
32
|
"simple_graph",
|
|
33
|
+
"StringLabelSchema",
|
|
28
34
|
"UNCERTAINTY_DTYPE",
|
|
29
35
|
"UndefinedInterface",
|
|
30
36
|
)
|
|
31
37
|
|
|
32
|
-
__version__ = "1.
|
|
38
|
+
__version__ = "1.4"
|
|
33
39
|
|
|
34
40
|
|
|
35
41
|
from bw_processing.array_creation import create_array, create_structured_array
|
|
36
42
|
from bw_processing.constants import DEFAULT_LICENSES, INDICES_DTYPE, UNCERTAINTY_DTYPE, MatrixSerializeFormat
|
|
43
|
+
from bw_processing.param_labels import (
|
|
44
|
+
AnyLabelSchema,
|
|
45
|
+
ParamLabelField,
|
|
46
|
+
ParamLabelSchema,
|
|
47
|
+
StringLabelSchema,
|
|
48
|
+
schema_from_json_schema,
|
|
49
|
+
)
|
|
37
50
|
from bw_processing.datapackage import (
|
|
38
51
|
Datapackage,
|
|
39
52
|
DatapackageBase,
|
|
@@ -46,7 +59,7 @@ from bw_processing.examples import examples_dir
|
|
|
46
59
|
from bw_processing.filesystem import clean_datapackage_name, md5, safe_filename
|
|
47
60
|
from bw_processing.indexing import reindex, reset_index
|
|
48
61
|
from bw_processing.io_helpers import generic_directory_filesystem, generic_zipfile_filesystem
|
|
49
|
-
from bw_processing.matrix_entry import MatrixEntry, MatrixName, create_datapackage_from_entries
|
|
62
|
+
from bw_processing.matrix_entry import ArrayEntry, MatrixEntry, MatrixName, create_datapackage_from_entries
|
|
50
63
|
from bw_processing.merging import merge_datapackages_with_mask
|
|
51
64
|
from bw_processing.proxies import UndefinedInterface
|
|
52
65
|
from bw_processing.unique_fields import as_unique_attributes, as_unique_attributes_dataframe
|