bw-processing 1.3__tar.gz → 1.5__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 (58) hide show
  1. {bw_processing-1.3/src/bw_processing.egg-info → bw_processing-1.5}/PKG-INFO +155 -1
  2. {bw_processing-1.3 → bw_processing-1.5}/README.md +153 -0
  3. {bw_processing-1.3 → bw_processing-1.5}/pyproject.toml +1 -0
  4. {bw_processing-1.3 → bw_processing-1.5}/src/bw_processing/__init__.py +15 -2
  5. {bw_processing-1.3 → bw_processing-1.5}/src/bw_processing/array_creation.py +29 -65
  6. {bw_processing-1.3 → bw_processing-1.5}/src/bw_processing/datapackage.py +415 -89
  7. {bw_processing-1.3 → bw_processing-1.5}/src/bw_processing/matrix_entry.py +69 -0
  8. bw_processing-1.5/src/bw_processing/param_labels.py +100 -0
  9. {bw_processing-1.3 → bw_processing-1.5}/src/bw_processing/utils.py +5 -1
  10. {bw_processing-1.3 → bw_processing-1.5/src/bw_processing.egg-info}/PKG-INFO +155 -1
  11. {bw_processing-1.3 → bw_processing-1.5}/src/bw_processing.egg-info/SOURCES.txt +2 -0
  12. {bw_processing-1.3 → bw_processing-1.5}/src/bw_processing.egg-info/requires.txt +1 -0
  13. bw_processing-1.5/tests/test_array_creation.py +97 -0
  14. {bw_processing-1.3 → bw_processing-1.5}/tests/test_datapackage.py +250 -51
  15. bw_processing-1.5/tests/test_matrix_entry.py +399 -0
  16. bw_processing-1.5/tests/test_params.py +483 -0
  17. {bw_processing-1.3 → bw_processing-1.5}/tests/test_utils.py +15 -3
  18. bw_processing-1.3/tests/test_array_creation.py +0 -14
  19. bw_processing-1.3/tests/test_matrix_entry.py +0 -178
  20. {bw_processing-1.3 → bw_processing-1.5}/LICENSE +0 -0
  21. {bw_processing-1.3 → bw_processing-1.5}/MANIFEST.in +0 -0
  22. {bw_processing-1.3 → bw_processing-1.5}/setup.cfg +0 -0
  23. {bw_processing-1.3 → bw_processing-1.5}/src/bw_processing/constants.py +0 -0
  24. {bw_processing-1.3 → bw_processing-1.5}/src/bw_processing/errors.py +0 -0
  25. {bw_processing-1.3 → bw_processing-1.5}/src/bw_processing/examples/__init__.py +0 -0
  26. {bw_processing-1.3 → bw_processing-1.5}/src/bw_processing/examples/datapackage_1/another name.indices.parquet +0 -0
  27. {bw_processing-1.3 → bw_processing-1.5}/src/bw_processing/examples/datapackage_1/sa-data-vector-from-dict.data.parquet +0 -0
  28. {bw_processing-1.3 → bw_processing-1.5}/src/bw_processing/examples/datapackage_1/sa-data-vector-from-dict.distributions.parquet +0 -0
  29. {bw_processing-1.3 → bw_processing-1.5}/src/bw_processing/examples/datapackage_1/sa-data-vector-from-dict.flip.parquet +0 -0
  30. {bw_processing-1.3 → bw_processing-1.5}/src/bw_processing/examples/datapackage_1/sa-data-vector-from-dict.indices.parquet +0 -0
  31. {bw_processing-1.3 → bw_processing-1.5}/src/bw_processing/examples/datapackage_1/some name.data.parquet +0 -0
  32. {bw_processing-1.3 → bw_processing-1.5}/src/bw_processing/examples/datapackage_1/some name.flip.parquet +0 -0
  33. {bw_processing-1.3 → bw_processing-1.5}/src/bw_processing/examples/datapackage_1/some name.indices.parquet +0 -0
  34. {bw_processing-1.3 → bw_processing-1.5}/src/bw_processing/examples/datapackage_2.zip +0 -0
  35. {bw_processing-1.3 → bw_processing-1.5}/src/bw_processing/examples/interfaces.py +0 -0
  36. {bw_processing-1.3 → bw_processing-1.5}/src/bw_processing/examples/parquet_files.py +0 -0
  37. {bw_processing-1.3 → bw_processing-1.5}/src/bw_processing/examples/simple.zip +0 -0
  38. {bw_processing-1.3 → bw_processing-1.5}/src/bw_processing/filesystem.py +0 -0
  39. {bw_processing-1.3 → bw_processing-1.5}/src/bw_processing/indexing.py +0 -0
  40. {bw_processing-1.3 → bw_processing-1.5}/src/bw_processing/io_helpers.py +0 -0
  41. {bw_processing-1.3 → bw_processing-1.5}/src/bw_processing/io_parquet_helpers.py +0 -0
  42. {bw_processing-1.3 → bw_processing-1.5}/src/bw_processing/io_pyarrow_helpers.py +0 -0
  43. {bw_processing-1.3 → bw_processing-1.5}/src/bw_processing/merging.py +0 -0
  44. {bw_processing-1.3 → bw_processing-1.5}/src/bw_processing/proxies.py +0 -0
  45. {bw_processing-1.3 → bw_processing-1.5}/src/bw_processing/unique_fields.py +0 -0
  46. {bw_processing-1.3 → bw_processing-1.5}/src/bw_processing.egg-info/dependency_links.txt +0 -0
  47. {bw_processing-1.3 → bw_processing-1.5}/src/bw_processing.egg-info/top_level.txt +0 -0
  48. {bw_processing-1.3 → bw_processing-1.5}/tests/test_filesystem.py +0 -0
  49. {bw_processing-1.3 → bw_processing-1.5}/tests/test_filtered_datapackage.py +0 -0
  50. {bw_processing-1.3 → bw_processing-1.5}/tests/test_indexing.py +0 -0
  51. {bw_processing-1.3 → bw_processing-1.5}/tests/test_integration.py +0 -0
  52. {bw_processing-1.3 → bw_processing-1.5}/tests/test_interfaces.py +0 -0
  53. {bw_processing-1.3 → bw_processing-1.5}/tests/test_io_parquet_helpers.py +0 -0
  54. {bw_processing-1.3 → bw_processing-1.5}/tests/test_io_pyarrow_helpers.py +0 -0
  55. {bw_processing-1.3 → bw_processing-1.5}/tests/test_loading.py +0 -0
  56. {bw_processing-1.3 → bw_processing-1.5}/tests/test_merging.py +0 -0
  57. {bw_processing-1.3 → bw_processing-1.5}/tests/test_proxies.py +0 -0
  58. {bw_processing-1.3 → bw_processing-1.5}/tests/test_unique_fields.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bw_processing
3
- Version: 1.3
3
+ Version: 1.5
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)
@@ -85,6 +93,7 @@ The [Brightway LCA framework](https://brightway.dev/) has stored data used in co
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.
87
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.
88
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.
89
98
 
90
99
  ## Concepts
@@ -230,6 +239,151 @@ dp.add_persistent_vector(
230
239
 
231
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`.
232
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
+
233
387
  ### Policies
234
388
 
235
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.
@@ -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)
@@ -43,6 +50,7 @@ The [Brightway LCA framework](https://brightway.dev/) has stored data used in co
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.
45
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.
46
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.
47
55
 
48
56
  ## Concepts
@@ -188,6 +196,151 @@ dp.add_persistent_vector(
188
196
 
189
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`.
190
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
+
191
344
  ### Policies
192
345
 
193
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.
@@ -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.3"
38
+ __version__ = "1.5"
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
@@ -21,46 +21,45 @@ def chunked(iterable, chunk_size):
21
21
  return iter(lambda: list(itertools.islice(iterable, chunk_size)), [])
22
22
 
23
23
 
24
- def create_chunked_structured_array(iterable, dtype, bucket_size=20000):
25
- """Create a numpy structured array from an iterable of indeterminate length.
24
+ def create_chunked(iterable, dtype, ncols=None, bucket_size=500):
25
+ """Create a numpy array from an iterable of indeterminate length.
26
+
27
+ Needed when we can't determine the length of the iterable ahead of time
28
+ (e.g. for a generator or a database cursor), so can't create the complete
29
+ array in memory in one step.
26
30
 
27
- Needed when we can't determine the length of the iterable ahead of time (e.g. for a generator or a database cursor), so can't create the complete array in memory in on step
31
+ Creates a list of arrays with ``bucket_size`` rows until ``iterable`` is
32
+ exhausted, then concatenates them along axis 0.
28
33
 
29
- Creates a list of arrays with ``bucket_size`` rows until ``iterable`` is exhausted, then concatenates them.
34
+ Pass ``ncols`` for a plain 2D array; omit it for a 1D structured array.
30
35
 
31
36
  Args:
32
37
  iterable: Iterable of data used to populate the array.
33
- dtype: Numpy dtype of the created array
34
- format_function: If provided, this function will be called on each row of ``iterable`` before insertion in the array.
38
+ dtype: Numpy dtype of the created array.
39
+ ncols: Number of columns; if None, a 1D structured array is created.
35
40
  bucket_size: Number of rows in each intermediate array.
36
41
 
37
- Returns:.
38
- Returns the created array. Will return a zero-length array if ``iterable`` has no data.
39
-
42
+ Returns:
43
+ The created array. Returns a zero-length array if ``iterable`` has no data.
40
44
  """
45
+ bucket_shape = (bucket_size,) if ncols is None else (bucket_size, ncols)
46
+ empty_shape = (0,) if ncols is None else (0, ncols)
41
47
  arrays = []
42
- array = np.zeros(bucket_size, dtype=dtype)
43
-
48
+ array = np.zeros(bucket_shape, dtype=dtype)
44
49
  for chunk in chunked(iterable, bucket_size):
45
50
  for i, row in enumerate(chunk):
46
51
  array[i] = row
47
52
  if i < bucket_size - 1:
48
- array = array[: i + 1]
49
- arrays.append(array)
53
+ # .copy() releases the oversized bucket buffer immediately rather
54
+ # than keeping it alive as a view until the final concatenation.
55
+ arrays.append(array[: i + 1].copy())
50
56
  else:
51
57
  arrays.append(array)
52
- array = np.zeros(bucket_size, dtype=dtype)
53
-
54
- # Empty iterable - create zero-length array
58
+ array = np.zeros(bucket_shape, dtype=dtype)
59
+ # Empty iterable - create zero-length array.
55
60
  # Needed because we return iterators for SQL databases
56
- # but don't know if e.g. sometime a database has
57
- # no biosphere exchanges
58
- if arrays:
59
- array = np.hstack(arrays)
60
- else:
61
- array = np.zeros(0, dtype=dtype)
62
-
63
- return array
61
+ # but don't know if e.g. sometimes a database has no biosphere exchanges.
62
+ return np.concatenate(arrays, axis=0) if arrays else np.zeros(empty_shape, dtype=dtype)
64
63
 
65
64
 
66
65
  def create_structured_array(iterable, dtype, nrows=None, sort=False, sort_fields=None):
@@ -80,7 +79,7 @@ def create_structured_array(iterable, dtype, nrows=None, sort=False, sort_fields
80
79
  array[i] = tuple(row)
81
80
 
82
81
  else:
83
- array = create_chunked_structured_array(iterable, dtype)
82
+ array = create_chunked(iterable, dtype)
84
83
 
85
84
  if sort:
86
85
  sort_fields = sort_fields or ()
@@ -93,44 +92,6 @@ def create_structured_array(iterable, dtype, nrows=None, sort=False, sort_fields
93
92
  return array
94
93
 
95
94
 
96
- def create_chunked_array(iterable, ncols, dtype=np.float32, bucket_size=500):
97
- """Create a numpy array from an iterable of indeterminate length.
98
-
99
- Needed when we can't determine the length of the iterable ahead of time (e.g. for a generator or a database cursor), so can't create the complete array in memory in on step
100
-
101
- Creates a list of arrays with ``bucket_size`` rows until ``iterable`` is exhausted, then concatenates them.
102
-
103
- Args:
104
- iterable: Iterable of data used to populate the array.
105
- ncols: Number of columns in the created array.
106
- dtype: Numpy dtype of the created array
107
- bucket_size: Number of rows in each intermediate array.
108
-
109
- Returns:.
110
- Returns the created array. Will return a zero-length array if ``iterable`` has no data.
111
-
112
- """
113
- arrays = []
114
- array = np.zeros((bucket_size, ncols), dtype=dtype)
115
-
116
- for chunk in chunked(iterable, bucket_size):
117
- for i, row in enumerate(chunk):
118
- array[i, :] = row
119
- if i < bucket_size - 1:
120
- array = array[: i + 1, :]
121
- arrays.append(array)
122
- else:
123
- arrays.append(array)
124
- array = np.zeros((bucket_size, ncols), dtype=dtype)
125
-
126
- if arrays:
127
- array = np.hstack(arrays)
128
- else:
129
- array = np.zeros((0, ncols), dtype=dtype)
130
-
131
- return array
132
-
133
-
134
95
  def create_array(iterable, nrows=None, dtype=np.float32):
135
96
  """Create a numpy array data ``iterable``. Returns a filepath of a created file (if ``filepath`` is provided, or the array.
136
97
 
@@ -152,7 +113,10 @@ def create_array(iterable, nrows=None, dtype=np.float32):
152
113
  array[i, :] = tuple(row)
153
114
 
154
115
  else:
155
- ncols, data = get_ncols(iterable)
156
- array = create_chunked_array(data, ncols, dtype)
116
+ try:
117
+ ncols, data = get_ncols(iterable)
118
+ except StopIteration:
119
+ return np.zeros((0, 0), dtype=dtype)
120
+ array = create_chunked(data, dtype, ncols=ncols)
157
121
 
158
122
  return array