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.
Files changed (57) hide show
  1. {bw_processing-1.2/src/bw_processing.egg-info → bw_processing-1.4}/PKG-INFO +199 -2
  2. {bw_processing-1.2 → bw_processing-1.4}/README.md +197 -1
  3. {bw_processing-1.2 → bw_processing-1.4}/pyproject.toml +1 -0
  4. {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/__init__.py +15 -2
  5. {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/array_creation.py +29 -65
  6. {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/constants.py +7 -4
  7. {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/datapackage.py +505 -15
  8. {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/io_helpers.py +35 -0
  9. {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/matrix_entry.py +67 -0
  10. bw_processing-1.4/src/bw_processing/param_labels.py +100 -0
  11. {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/proxies.py +8 -0
  12. {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/unique_fields.py +17 -0
  13. {bw_processing-1.2 → bw_processing-1.4/src/bw_processing.egg-info}/PKG-INFO +199 -2
  14. {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing.egg-info/SOURCES.txt +2 -0
  15. {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing.egg-info/requires.txt +1 -0
  16. bw_processing-1.4/tests/test_array_creation.py +97 -0
  17. {bw_processing-1.2 → bw_processing-1.4}/tests/test_datapackage.py +352 -1
  18. {bw_processing-1.2 → bw_processing-1.4}/tests/test_matrix_entry.py +119 -1
  19. bw_processing-1.4/tests/test_params.py +483 -0
  20. bw_processing-1.2/tests/test_array_creation.py +0 -14
  21. {bw_processing-1.2 → bw_processing-1.4}/LICENSE +0 -0
  22. {bw_processing-1.2 → bw_processing-1.4}/MANIFEST.in +0 -0
  23. {bw_processing-1.2 → bw_processing-1.4}/setup.cfg +0 -0
  24. {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/errors.py +0 -0
  25. {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/examples/__init__.py +0 -0
  26. {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/examples/datapackage_1/another name.indices.parquet +0 -0
  27. {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/examples/datapackage_1/sa-data-vector-from-dict.data.parquet +0 -0
  28. {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/examples/datapackage_1/sa-data-vector-from-dict.distributions.parquet +0 -0
  29. {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/examples/datapackage_1/sa-data-vector-from-dict.flip.parquet +0 -0
  30. {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/examples/datapackage_1/sa-data-vector-from-dict.indices.parquet +0 -0
  31. {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/examples/datapackage_1/some name.data.parquet +0 -0
  32. {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/examples/datapackage_1/some name.flip.parquet +0 -0
  33. {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/examples/datapackage_1/some name.indices.parquet +0 -0
  34. {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/examples/datapackage_2.zip +0 -0
  35. {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/examples/interfaces.py +0 -0
  36. {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/examples/parquet_files.py +0 -0
  37. {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/examples/simple.zip +0 -0
  38. {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/filesystem.py +0 -0
  39. {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/indexing.py +0 -0
  40. {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/io_parquet_helpers.py +0 -0
  41. {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/io_pyarrow_helpers.py +0 -0
  42. {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/merging.py +0 -0
  43. {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing/utils.py +0 -0
  44. {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing.egg-info/dependency_links.txt +0 -0
  45. {bw_processing-1.2 → bw_processing-1.4}/src/bw_processing.egg-info/top_level.txt +0 -0
  46. {bw_processing-1.2 → bw_processing-1.4}/tests/test_filesystem.py +0 -0
  47. {bw_processing-1.2 → bw_processing-1.4}/tests/test_filtered_datapackage.py +0 -0
  48. {bw_processing-1.2 → bw_processing-1.4}/tests/test_indexing.py +0 -0
  49. {bw_processing-1.2 → bw_processing-1.4}/tests/test_integration.py +0 -0
  50. {bw_processing-1.2 → bw_processing-1.4}/tests/test_interfaces.py +0 -0
  51. {bw_processing-1.2 → bw_processing-1.4}/tests/test_io_parquet_helpers.py +0 -0
  52. {bw_processing-1.2 → bw_processing-1.4}/tests/test_io_pyarrow_helpers.py +0 -0
  53. {bw_processing-1.2 → bw_processing-1.4}/tests/test_loading.py +0 -0
  54. {bw_processing-1.2 → bw_processing-1.4}/tests/test_merging.py +0 -0
  55. {bw_processing-1.2 → bw_processing-1.4}/tests/test_proxies.py +0 -0
  56. {bw_processing-1.2 → bw_processing-1.4}/tests/test_unique_fields.py +0 -0
  57. {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.2
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 `cmutel`). Depends on `numpy` and `pandas` (for reading and writing CSVs).
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 `cmutel`). Depends on `numpy` and `pandas` (for reading and writing CSVs).
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
 
@@ -32,6 +32,7 @@ dependencies = [
32
32
  # dependencies as strings with quotes, e.g. "foo"
33
33
  # You can add version requirements
34
34
  "fsspec",
35
+ "jsonschema>=4.0",
35
36
  "morefs",
36
37
  "numpy",
37
38
  "pandas",
@@ -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.2"
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