bw-processing 1.2__tar.gz → 1.3__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 (54) hide show
  1. {bw_processing-1.2/src/bw_processing.egg-info → bw_processing-1.3}/PKG-INFO +45 -2
  2. {bw_processing-1.2 → bw_processing-1.3}/README.md +44 -1
  3. {bw_processing-1.2 → bw_processing-1.3}/src/bw_processing/__init__.py +1 -1
  4. {bw_processing-1.2 → bw_processing-1.3}/src/bw_processing/constants.py +7 -4
  5. {bw_processing-1.2 → bw_processing-1.3}/src/bw_processing/datapackage.py +161 -5
  6. {bw_processing-1.2 → bw_processing-1.3}/src/bw_processing/io_helpers.py +35 -0
  7. {bw_processing-1.2 → bw_processing-1.3}/src/bw_processing/proxies.py +8 -0
  8. {bw_processing-1.2 → bw_processing-1.3}/src/bw_processing/unique_fields.py +17 -0
  9. {bw_processing-1.2 → bw_processing-1.3/src/bw_processing.egg-info}/PKG-INFO +45 -2
  10. {bw_processing-1.2 → bw_processing-1.3}/tests/test_datapackage.py +213 -1
  11. {bw_processing-1.2 → bw_processing-1.3}/LICENSE +0 -0
  12. {bw_processing-1.2 → bw_processing-1.3}/MANIFEST.in +0 -0
  13. {bw_processing-1.2 → bw_processing-1.3}/pyproject.toml +0 -0
  14. {bw_processing-1.2 → bw_processing-1.3}/setup.cfg +0 -0
  15. {bw_processing-1.2 → bw_processing-1.3}/src/bw_processing/array_creation.py +0 -0
  16. {bw_processing-1.2 → bw_processing-1.3}/src/bw_processing/errors.py +0 -0
  17. {bw_processing-1.2 → bw_processing-1.3}/src/bw_processing/examples/__init__.py +0 -0
  18. {bw_processing-1.2 → bw_processing-1.3}/src/bw_processing/examples/datapackage_1/another name.indices.parquet +0 -0
  19. {bw_processing-1.2 → bw_processing-1.3}/src/bw_processing/examples/datapackage_1/sa-data-vector-from-dict.data.parquet +0 -0
  20. {bw_processing-1.2 → bw_processing-1.3}/src/bw_processing/examples/datapackage_1/sa-data-vector-from-dict.distributions.parquet +0 -0
  21. {bw_processing-1.2 → bw_processing-1.3}/src/bw_processing/examples/datapackage_1/sa-data-vector-from-dict.flip.parquet +0 -0
  22. {bw_processing-1.2 → bw_processing-1.3}/src/bw_processing/examples/datapackage_1/sa-data-vector-from-dict.indices.parquet +0 -0
  23. {bw_processing-1.2 → bw_processing-1.3}/src/bw_processing/examples/datapackage_1/some name.data.parquet +0 -0
  24. {bw_processing-1.2 → bw_processing-1.3}/src/bw_processing/examples/datapackage_1/some name.flip.parquet +0 -0
  25. {bw_processing-1.2 → bw_processing-1.3}/src/bw_processing/examples/datapackage_1/some name.indices.parquet +0 -0
  26. {bw_processing-1.2 → bw_processing-1.3}/src/bw_processing/examples/datapackage_2.zip +0 -0
  27. {bw_processing-1.2 → bw_processing-1.3}/src/bw_processing/examples/interfaces.py +0 -0
  28. {bw_processing-1.2 → bw_processing-1.3}/src/bw_processing/examples/parquet_files.py +0 -0
  29. {bw_processing-1.2 → bw_processing-1.3}/src/bw_processing/examples/simple.zip +0 -0
  30. {bw_processing-1.2 → bw_processing-1.3}/src/bw_processing/filesystem.py +0 -0
  31. {bw_processing-1.2 → bw_processing-1.3}/src/bw_processing/indexing.py +0 -0
  32. {bw_processing-1.2 → bw_processing-1.3}/src/bw_processing/io_parquet_helpers.py +0 -0
  33. {bw_processing-1.2 → bw_processing-1.3}/src/bw_processing/io_pyarrow_helpers.py +0 -0
  34. {bw_processing-1.2 → bw_processing-1.3}/src/bw_processing/matrix_entry.py +0 -0
  35. {bw_processing-1.2 → bw_processing-1.3}/src/bw_processing/merging.py +0 -0
  36. {bw_processing-1.2 → bw_processing-1.3}/src/bw_processing/utils.py +0 -0
  37. {bw_processing-1.2 → bw_processing-1.3}/src/bw_processing.egg-info/SOURCES.txt +0 -0
  38. {bw_processing-1.2 → bw_processing-1.3}/src/bw_processing.egg-info/dependency_links.txt +0 -0
  39. {bw_processing-1.2 → bw_processing-1.3}/src/bw_processing.egg-info/requires.txt +0 -0
  40. {bw_processing-1.2 → bw_processing-1.3}/src/bw_processing.egg-info/top_level.txt +0 -0
  41. {bw_processing-1.2 → bw_processing-1.3}/tests/test_array_creation.py +0 -0
  42. {bw_processing-1.2 → bw_processing-1.3}/tests/test_filesystem.py +0 -0
  43. {bw_processing-1.2 → bw_processing-1.3}/tests/test_filtered_datapackage.py +0 -0
  44. {bw_processing-1.2 → bw_processing-1.3}/tests/test_indexing.py +0 -0
  45. {bw_processing-1.2 → bw_processing-1.3}/tests/test_integration.py +0 -0
  46. {bw_processing-1.2 → bw_processing-1.3}/tests/test_interfaces.py +0 -0
  47. {bw_processing-1.2 → bw_processing-1.3}/tests/test_io_parquet_helpers.py +0 -0
  48. {bw_processing-1.2 → bw_processing-1.3}/tests/test_io_pyarrow_helpers.py +0 -0
  49. {bw_processing-1.2 → bw_processing-1.3}/tests/test_loading.py +0 -0
  50. {bw_processing-1.2 → bw_processing-1.3}/tests/test_matrix_entry.py +0 -0
  51. {bw_processing-1.2 → bw_processing-1.3}/tests/test_merging.py +0 -0
  52. {bw_processing-1.2 → bw_processing-1.3}/tests/test_proxies.py +0 -0
  53. {bw_processing-1.2 → bw_processing-1.3}/tests/test_unique_fields.py +0 -0
  54. {bw_processing-1.2 → bw_processing-1.3}/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.3
4
4
  Summary: Foo
5
5
  Author-email: Chris Mutel <cmutel@gmail.com>
6
6
  Maintainer-email: Chris Mutel <cmutel@gmail.com>
@@ -84,6 +84,7 @@ The [Brightway LCA framework](https://brightway.dev/) has stored data used in co
84
84
  * **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
85
  * **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
86
  * **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
+ * **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.
87
88
  * **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
89
 
89
90
  ## Concepts
@@ -96,6 +97,18 @@ Data objects can be vectors or arrays. Vectors will always produce the same matr
96
97
 
97
98
  ### Vectors versus arrays
98
99
 
100
+ Vectors and arrays differ in how many possible values they provide per matrix cell.
101
+
102
+ 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.
103
+
104
+ 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:
105
+
106
+ * **Monte Carlo analysis** — columns hold independently sampled values drawn from the uncertainty distributions.
107
+ * **Scenario analysis** — each column represents a predefined scenario, such as a different technology mix or policy assumption.
108
+ * **Presamples** — a generalization of the [presamples library](https://github.com/PascalLesage/presamples/), where pre-drawn samples are stored for reproducibility.
109
+
110
+ Which column is selected on each iteration is controlled by the `sequential` and `combinatorial` policies; see [Policies](#policies).
111
+
99
112
  ### Persistent versus dynamic
100
113
 
101
114
  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 +200,36 @@ print(data_obj.url)
187
200
  >>> "example.com"
188
201
  ```
189
202
 
203
+ ### Scale arrays
204
+
205
+ 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.
206
+
207
+ Typical use cases:
208
+
209
+ * **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.
210
+ * **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.
211
+
212
+ ```python
213
+ import numpy as np
214
+ from bw_processing import create_datapackage
215
+ from bw_processing.constants import INDICES_DTYPE
216
+
217
+ dp = create_datapackage()
218
+ indices_array = np.array([(1, 4), (2, 5), (3, 6)], dtype=INDICES_DTYPE)
219
+ data_array = np.array([100.0, 200.0, 300.0])
220
+ scale_array = np.array([0.6, 1.0, 0.4]) # e.g. allocation factors
221
+
222
+ dp.add_persistent_vector(
223
+ matrix="technosphere",
224
+ name="my-process",
225
+ indices_array=indices_array,
226
+ data_array=data_array,
227
+ scale_array=scale_array,
228
+ )
229
+ ```
230
+
231
+ 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
+
190
233
  ### Policies
191
234
 
192
235
  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 +262,7 @@ Please make sure you understand how `combinatorial` and `sequential` interact! T
219
262
 
220
263
  ## Install
221
264
 
222
- Install using pip or conda (channel `cmutel`). Depends on `numpy` and `pandas` (for reading and writing CSVs).
265
+ Install using pip or conda (channel `conda-forge`). Depends on `numpy` and `pandas` (for reading and writing CSVs).
223
266
 
224
267
  Has no explicit or implicit dependence on any other part of Brightway.
225
268
 
@@ -42,6 +42,7 @@ The [Brightway LCA framework](https://brightway.dev/) has stored data used in co
42
42
  * **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
43
  * **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
44
  * **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
+ * **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.
45
46
  * **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
47
 
47
48
  ## Concepts
@@ -54,6 +55,18 @@ Data objects can be vectors or arrays. Vectors will always produce the same matr
54
55
 
55
56
  ### Vectors versus arrays
56
57
 
58
+ Vectors and arrays differ in how many possible values they provide per matrix cell.
59
+
60
+ 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.
61
+
62
+ 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:
63
+
64
+ * **Monte Carlo analysis** — columns hold independently sampled values drawn from the uncertainty distributions.
65
+ * **Scenario analysis** — each column represents a predefined scenario, such as a different technology mix or policy assumption.
66
+ * **Presamples** — a generalization of the [presamples library](https://github.com/PascalLesage/presamples/), where pre-drawn samples are stored for reproducibility.
67
+
68
+ Which column is selected on each iteration is controlled by the `sequential` and `combinatorial` policies; see [Policies](#policies).
69
+
57
70
  ### Persistent versus dynamic
58
71
 
59
72
  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 +158,36 @@ print(data_obj.url)
145
158
  >>> "example.com"
146
159
  ```
147
160
 
161
+ ### Scale arrays
162
+
163
+ 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.
164
+
165
+ Typical use cases:
166
+
167
+ * **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.
168
+ * **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.
169
+
170
+ ```python
171
+ import numpy as np
172
+ from bw_processing import create_datapackage
173
+ from bw_processing.constants import INDICES_DTYPE
174
+
175
+ dp = create_datapackage()
176
+ indices_array = np.array([(1, 4), (2, 5), (3, 6)], dtype=INDICES_DTYPE)
177
+ data_array = np.array([100.0, 200.0, 300.0])
178
+ scale_array = np.array([0.6, 1.0, 0.4]) # e.g. allocation factors
179
+
180
+ dp.add_persistent_vector(
181
+ matrix="technosphere",
182
+ name="my-process",
183
+ indices_array=indices_array,
184
+ data_array=data_array,
185
+ scale_array=scale_array,
186
+ )
187
+ ```
188
+
189
+ 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
+
148
191
  ### Policies
149
192
 
150
193
  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 +220,7 @@ Please make sure you understand how `combinatorial` and `sequential` interact! T
177
220
 
178
221
  ## Install
179
222
 
180
- Install using pip or conda (channel `cmutel`). Depends on `numpy` and `pandas` (for reading and writing CSVs).
223
+ Install using pip or conda (channel `conda-forge`). Depends on `numpy` and `pandas` (for reading and writing CSVs).
181
224
 
182
225
  Has no explicit or implicit dependence on any other part of Brightway.
183
226
 
@@ -29,7 +29,7 @@ __all__ = (
29
29
  "UndefinedInterface",
30
30
  )
31
31
 
32
- __version__ = "1.2"
32
+ __version__ = "1.3"
33
33
 
34
34
 
35
35
  from bw_processing.array_creation import create_array, create_structured_array
@@ -35,12 +35,15 @@ DEFAULT_LICENSES = [
35
35
 
36
36
 
37
37
  class MatrixSerializeFormat(str, Enum):
38
- """
39
- Enum with the serializing formats for the vectors and matrices.
38
+ """Serialization format used when writing numpy arrays to disk.
39
+
40
+ Because this is a ``str`` enum, values can be compared directly to strings.
41
+ The default is ``NUMPY``. ``PARQUET`` requires the optional ``pyarrow``
42
+ dependency.
40
43
  """
41
44
 
42
- NUMPY = "numpy" # numpy .npy format
43
- PARQUET = "parquet" # Apache .parquet format
45
+ NUMPY = "numpy"
46
+ PARQUET = "parquet"
44
47
 
45
48
 
46
49
  # FILE EXTENSIONS
@@ -384,6 +384,17 @@ class Datapackage(DatapackageBase):
384
384
  self.data = []
385
385
 
386
386
  def finalize_serialization(self) -> None:
387
+ """Write the metadata file and close the filesystem.
388
+
389
+ Must be called once after all resources have been added, before the
390
+ datapackage can be loaded again from disk. Dehydrates any interface
391
+ resources (replaces them with ``UndefinedInterface``) prior to writing.
392
+
393
+ Raises:
394
+ Closed: Datapackage has already been finalized.
395
+ ValueError: Datapackage uses an in-memory filesystem, which cannot
396
+ be serialized.
397
+ """
387
398
  if self._finalized:
388
399
  raise Closed("Datapackage already finalized")
389
400
  elif isinstance(self.fs, DictFS):
@@ -485,11 +496,20 @@ class Datapackage(DatapackageBase):
485
496
  data_array: Optional[np.ndarray] = None,
486
497
  flip_array: Optional[np.ndarray] = None,
487
498
  distributions_array: Optional[np.ndarray] = None,
499
+ scale_array: Optional[np.ndarray] = None,
488
500
  keep_proxy: bool = False,
489
501
  matrix_serialize_format_type: Optional[MatrixSerializeFormat] = None,
490
502
  **kwargs,
491
503
  ) -> None:
492
- """ """
504
+ """Add a persistent vector resource group to the datapackage.
505
+
506
+ ``scale_array`` is an optional 1-D float array of the same length as
507
+ ``indices_array``. Each element is a multiplicative factor applied to
508
+ the corresponding data value — whether static or stochastic — before
509
+ the value is inserted into the matrix. Typical uses are allocation
510
+ factors and unit conversions. A value of ``1.0`` leaves the data
511
+ unchanged.
512
+ """
493
513
  self._prepare_modifications()
494
514
 
495
515
  # Check lengths
@@ -580,6 +600,15 @@ class Datapackage(DatapackageBase):
580
600
  meta_type="generic",
581
601
  **kwargs,
582
602
  )
603
+ if scale_array is not None:
604
+ self._add_scale_array_resource(
605
+ scale_array=scale_array,
606
+ indices_array=indices_array,
607
+ name=name,
608
+ keep_proxy=keep_proxy,
609
+ matrix_serialize_format_type=matrix_serialize_format_type,
610
+ **kwargs,
611
+ )
583
612
 
584
613
  def add_persistent_array(
585
614
  self,
@@ -589,11 +618,20 @@ class Datapackage(DatapackageBase):
589
618
  indices_array: np.ndarray,
590
619
  name: Optional[str] = None,
591
620
  flip_array: Optional[np.ndarray] = None,
621
+ scale_array: Optional[np.ndarray] = None,
592
622
  keep_proxy: bool = False,
593
623
  matrix_serialize_format_type: Optional[MatrixSerializeFormat] = None,
594
624
  **kwargs,
595
625
  ) -> None:
596
- """ """
626
+ """Add a persistent array resource group to the datapackage.
627
+
628
+ ``scale_array`` is an optional 1-D float array of the same length as
629
+ ``indices_array``. Each element is a multiplicative factor applied to
630
+ the corresponding data value — whether static or stochastic — before
631
+ the value is inserted into the matrix. Typical uses are allocation
632
+ factors and unit conversions. A value of ``1.0`` leaves the data
633
+ unchanged.
634
+ """
597
635
  self._prepare_modifications()
598
636
 
599
637
  kwargs.update({"matrix": matrix, "category": "array", "nrows": len(indices_array)})
@@ -660,9 +698,23 @@ class Datapackage(DatapackageBase):
660
698
  meta_type="generic",
661
699
  **kwargs,
662
700
  )
701
+ if scale_array is not None:
702
+ self._add_scale_array_resource(
703
+ scale_array=scale_array,
704
+ indices_array=indices_array,
705
+ name=name,
706
+ keep_proxy=keep_proxy,
707
+ matrix_serialize_format_type=matrix_serialize_format_type,
708
+ **kwargs,
709
+ )
663
710
 
664
711
  def write_modified(self):
665
- """Write the data in modified files to the filesystem (if allowed)."""
712
+ """Flush modified resources back to the filesystem.
713
+
714
+ After directly editing data arrays in ``self.data``, call this method
715
+ to persist the changes. Clears the internal ``_modified`` set on
716
+ completion. Does nothing if no resources have been marked as modified.
717
+ """
666
718
  for index in self._modified:
667
719
  # get resource
668
720
  resource = self.resources[index]
@@ -683,7 +735,7 @@ class Datapackage(DatapackageBase):
683
735
  if kind == "indices":
684
736
  meta_object = "vector"
685
737
  meta_type = "indices"
686
- elif kind == "flip":
738
+ elif kind in ("flip", "scale"):
687
739
  meta_object = "vector"
688
740
  meta_type = "generic"
689
741
  elif kind == "distributions":
@@ -716,6 +768,39 @@ class Datapackage(DatapackageBase):
716
768
 
717
769
  self._modified = set()
718
770
 
771
+ def _add_scale_array_resource(
772
+ self,
773
+ *,
774
+ scale_array: np.ndarray,
775
+ indices_array: np.ndarray,
776
+ name: str,
777
+ keep_proxy: bool,
778
+ matrix_serialize_format_type: Optional[MatrixSerializeFormat],
779
+ **kwargs,
780
+ ) -> None:
781
+ scale_array = load_bytes(scale_array)
782
+ if not np.issubdtype(scale_array.dtype, np.floating):
783
+ raise WrongDatatype(
784
+ "`scale_array` dtype is {}, but must be a float dtype".format(scale_array.dtype)
785
+ )
786
+ elif scale_array.shape != indices_array.shape:
787
+ raise ShapeMismatch(
788
+ "`scale_array` shape ({}) doesn't match `indices_array` ({}).".format(
789
+ scale_array.shape, indices_array.shape
790
+ )
791
+ )
792
+ self._add_numpy_array_resource(
793
+ array=scale_array,
794
+ group=name,
795
+ name=name + ".scale",
796
+ kind="scale",
797
+ keep_proxy=keep_proxy,
798
+ matrix_serialize_format_type=matrix_serialize_format_type,
799
+ meta_object="vector",
800
+ meta_type="generic",
801
+ **kwargs,
802
+ )
803
+
719
804
  def _add_numpy_array_resource(
720
805
  self,
721
806
  *,
@@ -798,10 +883,36 @@ class Datapackage(DatapackageBase):
798
883
  indices_array: np.ndarray, # Not interface
799
884
  name: Optional[str] = None,
800
885
  flip_array: Optional[np.ndarray] = None, # Not interface
886
+ scale_array: Optional[np.ndarray] = None, # Not interface
801
887
  keep_proxy: bool = False,
802
888
  matrix_serialize_format_type: Optional[MatrixSerializeFormat] = None,
803
889
  **kwargs,
804
890
  ) -> None:
891
+ """Add a dynamic vector resource group to the datapackage.
892
+
893
+ The matrix values are provided at runtime by ``interface`` rather than
894
+ stored on disk. ``interface`` must implement ``__next__()`` and return a
895
+ 1-D numpy array of length ``len(indices_array)`` each time it is called.
896
+
897
+ The ``indices_array``, optional ``flip_array``, and optional
898
+ ``scale_array`` are static and are stored as normal numpy resources.
899
+
900
+ Args:
901
+ matrix: Name of the target matrix.
902
+ interface: Object implementing the dynamic-vector interface
903
+ (``__next__()``).
904
+ indices_array: Structured numpy array with dtype ``INDICES_DTYPE``
905
+ mapping each data value to a matrix cell.
906
+ name: Optional resource group name; auto-generated if omitted.
907
+ flip_array: Optional boolean array; where ``True`` the value is
908
+ multiplied by ``-1`` before insertion.
909
+ scale_array: Optional 1-D float array of multiplicative factors
910
+ applied before matrix insertion.
911
+ keep_proxy: If ``True``, store a proxy rather than the raw array
912
+ for on-disk resources.
913
+ matrix_serialize_format_type: Override the instance-level
914
+ serialization format for static arrays in this group.
915
+ """
805
916
  self._prepare_modifications()
806
917
 
807
918
  kwargs.update({"matrix": matrix, "category": "vector", "nrows": len(indices_array)})
@@ -843,6 +954,15 @@ class Datapackage(DatapackageBase):
843
954
  meta_type="generic",
844
955
  **kwargs,
845
956
  )
957
+ if scale_array is not None:
958
+ self._add_scale_array_resource(
959
+ scale_array=scale_array,
960
+ indices_array=indices_array,
961
+ name=name,
962
+ keep_proxy=keep_proxy,
963
+ matrix_serialize_format_type=matrix_serialize_format_type,
964
+ **kwargs,
965
+ )
846
966
 
847
967
  self.data.append(interface)
848
968
  resource = {
@@ -862,11 +982,38 @@ class Datapackage(DatapackageBase):
862
982
  indices_array: np.ndarray, # Not interface
863
983
  name: Optional[str] = None,
864
984
  flip_array: Optional[np.ndarray] = None,
985
+ scale_array: Optional[np.ndarray] = None, # Not interface
865
986
  keep_proxy: bool = False,
866
987
  matrix_serialize_format_type: Optional[MatrixSerializeFormat] = None,
867
988
  **kwargs,
868
989
  ) -> None:
869
- """`interface` must support the presamples API."""
990
+ """Add a dynamic array resource group to the datapackage.
991
+
992
+ The matrix values are provided at runtime by ``interface``, which must
993
+ implement the presamples array API: a ``.shape`` property returning
994
+ ``(nrows, ncols)`` and ``.__getitem__(args)`` returning the 1-D column
995
+ array for ``args[1]``. ``ncols`` may be ``None`` for an infinite
996
+ interface.
997
+
998
+ The ``indices_array``, optional ``flip_array``, and optional
999
+ ``scale_array`` are static and are stored as normal numpy resources.
1000
+
1001
+ Args:
1002
+ matrix: Name of the target matrix.
1003
+ interface: Object implementing the dynamic-array interface
1004
+ (``.shape`` and ``.__getitem__``).
1005
+ indices_array: Structured numpy array with dtype ``INDICES_DTYPE``
1006
+ mapping each data value to a matrix cell.
1007
+ name: Optional resource group name; auto-generated if omitted.
1008
+ flip_array: Optional boolean array; where ``True`` the value is
1009
+ multiplied by ``-1`` before insertion.
1010
+ scale_array: Optional 1-D float array of multiplicative factors
1011
+ applied before matrix insertion.
1012
+ keep_proxy: If ``True``, store a proxy rather than the raw array
1013
+ for on-disk resources.
1014
+ matrix_serialize_format_type: Override the instance-level
1015
+ serialization format for static arrays in this group.
1016
+ """
870
1017
  self._prepare_modifications()
871
1018
 
872
1019
  if isinstance(flip_array, np.ndarray) and not flip_array.sum():
@@ -911,6 +1058,15 @@ class Datapackage(DatapackageBase):
911
1058
  meta_type="generic",
912
1059
  **kwargs,
913
1060
  )
1061
+ if scale_array is not None:
1062
+ self._add_scale_array_resource(
1063
+ scale_array=scale_array,
1064
+ indices_array=indices_array,
1065
+ name=name,
1066
+ keep_proxy=keep_proxy,
1067
+ matrix_serialize_format_type=matrix_serialize_format_type,
1068
+ **kwargs,
1069
+ )
914
1070
 
915
1071
  self.data.append(interface)
916
1072
  resource = {
@@ -27,6 +27,19 @@ except ImportError:
27
27
 
28
28
 
29
29
  def generic_directory_filesystem(*, dirpath: Path) -> DirFileSystem:
30
+ """Return a ``DirFileSystem`` rooted at ``dirpath``, creating it if needed.
31
+
32
+ Args:
33
+ dirpath: Path to the target directory. Created if it does not exist;
34
+ its parent must already exist.
35
+
36
+ Returns:
37
+ A ``fsspec`` ``DirFileSystem`` pointing at ``dirpath``.
38
+
39
+ Raises:
40
+ ValueError: Parent directory does not exist.
41
+ AssertionError: ``dirpath`` is not a ``pathlib.Path``.
42
+ """
30
43
  assert isinstance(dirpath, Path), "`dirpath` must be a `pathlib.Path` instance"
31
44
  if not dirpath.is_dir():
32
45
  if not dirpath.parent.is_dir():
@@ -43,6 +56,28 @@ def generic_zipfile_filesystem(
43
56
  compression: int = zipfile.ZIP_DEFLATED,
44
57
  compresslevel: Optional[int] = None,
45
58
  ) -> ZipFileSystem:
59
+ """Return a ``ZipFileSystem`` for ``dirpath / filename``.
60
+
61
+ Args:
62
+ dirpath: Directory that contains (or will contain) the zip file.
63
+ Must already exist.
64
+ filename: Name of the zip file (e.g. ``"my-dp.zip"``).
65
+ write: If ``True`` (default), open for writing; otherwise open for
66
+ reading.
67
+ compression: ``zipfile`` compression constant. Defaults to
68
+ ``zipfile.ZIP_DEFLATED`` which gives good compression speed.
69
+ Pass ``zipfile.ZIP_STORED`` for no compression or
70
+ ``zipfile.ZIP_LZMA`` for maximum compression.
71
+ compresslevel: Compression level passed to ``zipfile.ZipFile``;
72
+ ``None`` uses the default for the chosen algorithm.
73
+
74
+ Returns:
75
+ A ``fsspec`` ``ZipFileSystem``.
76
+
77
+ Raises:
78
+ ValueError: ``dirpath`` does not exist.
79
+ AssertionError: ``dirpath`` is not a ``pathlib.Path``.
80
+ """
46
81
  assert isinstance(dirpath, Path), "`dirpath` must be a `pathlib.Path` instance"
47
82
  if not dirpath.is_dir():
48
83
  raise ValueError("Destination directory `{}` doesn't exist".format(dirpath))
@@ -5,6 +5,14 @@ class UndefinedInterface:
5
5
 
6
6
 
7
7
  class Proxy:
8
+ """Deferred file-read wrapper returned when ``proxy=True`` is passed to ``file_reader``.
9
+
10
+ Stores the reader function and its arguments without executing them. The
11
+ actual data is loaded the first time the proxy is called (i.e. when
12
+ ``get_resource`` resolves it). The file or buffer is rewound to position 0
13
+ before each call so that repeated calls return the same data.
14
+ """
15
+
8
16
  def __init__(self, func, label, kwargs):
9
17
  self.func = func
10
18
  self.label = label
@@ -60,6 +60,23 @@ def greedy_set_cover(data, exclude=None, raise_error=True):
60
60
 
61
61
 
62
62
  def as_unique_attributes_dataframe(df, exclude=None, include=None, raise_error=False):
63
+ """Return a copy of ``df`` keeping only the columns needed to uniquely identify each row.
64
+
65
+ Columns are selected using the greedy set-cover heuristic (see
66
+ :func:`greedy_set_cover`). Columns listed in ``include`` are always kept,
67
+ even if they are not required for uniqueness.
68
+
69
+ Args:
70
+ df: Input pandas DataFrame.
71
+ exclude: Column names to skip during the uniqueness search.
72
+ include: Column names to always retain in the output.
73
+ raise_error: If ``True``, raise ``NonUnique`` when no unique set can be
74
+ found; otherwise, all columns are kept silently.
75
+
76
+ Returns:
77
+ A DataFrame with only the uniqueness-covering (and always-included)
78
+ columns.
79
+ """
63
80
  assert isinstance(df, pd.DataFrame)
64
81
  include = greedy_set_cover(
65
82
  df.reset_index().to_dict("records"), exclude=exclude, raise_error=raise_error
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bw_processing
3
- Version: 1.2
3
+ Version: 1.3
4
4
  Summary: Foo
5
5
  Author-email: Chris Mutel <cmutel@gmail.com>
6
6
  Maintainer-email: Chris Mutel <cmutel@gmail.com>
@@ -84,6 +84,7 @@ The [Brightway LCA framework](https://brightway.dev/) has stored data used in co
84
84
  * **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
85
  * **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
86
  * **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
+ * **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.
87
88
  * **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
89
 
89
90
  ## Concepts
@@ -96,6 +97,18 @@ Data objects can be vectors or arrays. Vectors will always produce the same matr
96
97
 
97
98
  ### Vectors versus arrays
98
99
 
100
+ Vectors and arrays differ in how many possible values they provide per matrix cell.
101
+
102
+ 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.
103
+
104
+ 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:
105
+
106
+ * **Monte Carlo analysis** — columns hold independently sampled values drawn from the uncertainty distributions.
107
+ * **Scenario analysis** — each column represents a predefined scenario, such as a different technology mix or policy assumption.
108
+ * **Presamples** — a generalization of the [presamples library](https://github.com/PascalLesage/presamples/), where pre-drawn samples are stored for reproducibility.
109
+
110
+ Which column is selected on each iteration is controlled by the `sequential` and `combinatorial` policies; see [Policies](#policies).
111
+
99
112
  ### Persistent versus dynamic
100
113
 
101
114
  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 +200,36 @@ print(data_obj.url)
187
200
  >>> "example.com"
188
201
  ```
189
202
 
203
+ ### Scale arrays
204
+
205
+ 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.
206
+
207
+ Typical use cases:
208
+
209
+ * **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.
210
+ * **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.
211
+
212
+ ```python
213
+ import numpy as np
214
+ from bw_processing import create_datapackage
215
+ from bw_processing.constants import INDICES_DTYPE
216
+
217
+ dp = create_datapackage()
218
+ indices_array = np.array([(1, 4), (2, 5), (3, 6)], dtype=INDICES_DTYPE)
219
+ data_array = np.array([100.0, 200.0, 300.0])
220
+ scale_array = np.array([0.6, 1.0, 0.4]) # e.g. allocation factors
221
+
222
+ dp.add_persistent_vector(
223
+ matrix="technosphere",
224
+ name="my-process",
225
+ indices_array=indices_array,
226
+ data_array=data_array,
227
+ scale_array=scale_array,
228
+ )
229
+ ```
230
+
231
+ 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
+
190
233
  ### Policies
191
234
 
192
235
  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 +262,7 @@ Please make sure you understand how `combinatorial` and `sequential` interact! T
219
262
 
220
263
  ## Install
221
264
 
222
- Install using pip or conda (channel `cmutel`). Depends on `numpy` and `pandas` (for reading and writing CSVs).
265
+ Install using pip or conda (channel `conda-forge`). Depends on `numpy` and `pandas` (for reading and writing CSVs).
223
266
 
224
267
  Has no explicit or implicit dependence on any other part of Brightway.
225
268
 
@@ -8,7 +8,7 @@ import pytest
8
8
  from fsspec.implementations.zip import ZipFileSystem
9
9
  from morefs.dict import DictFS
10
10
 
11
- from bw_processing import create_datapackage, load_datapackage, simple_graph
11
+ from bw_processing import create_datapackage, load_datapackage, simple_graph, MatrixSerializeFormat
12
12
  from bw_processing.constants import INDICES_DTYPE, UNCERTAINTY_DTYPE, MAX_SIGNED_64BIT_INT, MAX_SIGNED_32BIT_INT
13
13
  from bw_processing.errors import NonUnique, PotentialInconsistency, ShapeMismatch, WrongDatatype
14
14
  from bw_processing.io_helpers import generic_directory_filesystem, generic_zipfile_filesystem
@@ -457,6 +457,218 @@ def test_add_dynamic_vector_flip_shapemistmatch():
457
457
  )
458
458
 
459
459
 
460
+ def test_add_persistent_vector_scale_array():
461
+ dp = create_datapackage()
462
+ data_array = np.array([2.0, 7.0, 12.0])
463
+ scale_array = np.array([0.5, 1.0, 2.0])
464
+ indices_array = np.array([(1, 4), (2, 5), (3, 6)], dtype=INDICES_DTYPE)
465
+ dp.add_persistent_vector(
466
+ matrix="sa_matrix",
467
+ data_array=data_array,
468
+ name="sa-data-vector",
469
+ indices_array=indices_array,
470
+ scale_array=scale_array,
471
+ )
472
+ assert "sa-data-vector.scale" in [o["name"] for o in dp.resources]
473
+ data, meta = dp.get_resource("sa-data-vector.scale")
474
+ assert meta["kind"] == "scale"
475
+ assert np.allclose(data, scale_array)
476
+
477
+
478
+ def test_add_persistent_vector_scale_dtype():
479
+ dp = create_datapackage()
480
+ data_array = np.array([2.0, 7.0, 12.0])
481
+ scale_array = np.array([1, 2, 3]) # integer dtype
482
+ indices_array = np.array([(1, 4), (2, 5), (3, 6)], dtype=INDICES_DTYPE)
483
+ with pytest.raises(WrongDatatype):
484
+ dp.add_persistent_vector(
485
+ matrix="sa_matrix",
486
+ data_array=data_array,
487
+ name="sa-data-vector",
488
+ indices_array=indices_array,
489
+ scale_array=scale_array,
490
+ )
491
+
492
+
493
+ def test_add_persistent_vector_scale_shapemismatch():
494
+ dp = create_datapackage()
495
+ data_array = np.array([2.0, 7.0, 12.0])
496
+ scale_array = np.array([0.5, 1.0, 2.0, 3.0])
497
+ indices_array = np.array([(1, 4), (2, 5), (3, 6)], dtype=INDICES_DTYPE)
498
+ with pytest.raises(ShapeMismatch):
499
+ dp.add_persistent_vector(
500
+ matrix="sa_matrix",
501
+ data_array=data_array,
502
+ name="sa-data-vector",
503
+ indices_array=indices_array,
504
+ scale_array=scale_array,
505
+ )
506
+
507
+
508
+ def test_add_persistent_array_scale_array():
509
+ dp = create_datapackage()
510
+ data_array = np.arange(12, dtype=float).reshape(3, 4)
511
+ scale_array = np.array([0.5, 1.0, 2.0])
512
+ indices_array = np.array([(1, 4), (2, 5), (3, 6)], dtype=INDICES_DTYPE)
513
+ dp.add_persistent_array(
514
+ matrix="sa_matrix",
515
+ data_array=data_array,
516
+ name="sa-data-vector",
517
+ indices_array=indices_array,
518
+ scale_array=scale_array,
519
+ )
520
+ assert "sa-data-vector.scale" in [o["name"] for o in dp.resources]
521
+ data, meta = dp.get_resource("sa-data-vector.scale")
522
+ assert meta["kind"] == "scale"
523
+ assert np.allclose(data, scale_array)
524
+
525
+
526
+ def test_add_persistent_array_scale_dtype():
527
+ dp = create_datapackage()
528
+ data_array = np.arange(12, dtype=float).reshape(3, 4)
529
+ scale_array = np.array([1, 2, 3])
530
+ indices_array = np.array([(1, 4), (2, 5), (3, 6)], dtype=INDICES_DTYPE)
531
+ with pytest.raises(WrongDatatype):
532
+ dp.add_persistent_array(
533
+ matrix="sa_matrix",
534
+ data_array=data_array,
535
+ name="sa-data-vector",
536
+ indices_array=indices_array,
537
+ scale_array=scale_array,
538
+ )
539
+
540
+
541
+ def test_add_persistent_array_scale_shapemismatch():
542
+ dp = create_datapackage()
543
+ data_array = np.arange(12, dtype=float).reshape(3, 4)
544
+ scale_array = np.array([0.5, 1.0, 2.0, 3.0])
545
+ indices_array = np.array([(1, 4), (2, 5), (3, 6)], dtype=INDICES_DTYPE)
546
+ with pytest.raises(ShapeMismatch):
547
+ dp.add_persistent_array(
548
+ matrix="sa_matrix",
549
+ data_array=data_array,
550
+ name="sa-data-vector",
551
+ indices_array=indices_array,
552
+ scale_array=scale_array,
553
+ )
554
+
555
+
556
+ def test_add_dynamic_vector_scale_array():
557
+ dp = create_datapackage()
558
+ scale_array = np.array([0.5, 1.0, 2.0])
559
+ indices_array = np.array([(1, 4), (2, 5), (3, 6)], dtype=INDICES_DTYPE)
560
+ dp.add_dynamic_vector(
561
+ matrix="sa_matrix",
562
+ interface=Dummy(),
563
+ name="sa-data-vector",
564
+ indices_array=indices_array,
565
+ scale_array=scale_array,
566
+ )
567
+ assert "sa-data-vector.scale" in [o["name"] for o in dp.resources]
568
+ data, meta = dp.get_resource("sa-data-vector.scale")
569
+ assert meta["kind"] == "scale"
570
+ assert np.allclose(data, scale_array)
571
+
572
+
573
+ def test_add_dynamic_vector_scale_dtype():
574
+ dp = create_datapackage()
575
+ scale_array = np.array([1, 2, 3])
576
+ indices_array = np.array([(1, 4), (2, 5), (3, 6)], dtype=INDICES_DTYPE)
577
+ with pytest.raises(WrongDatatype):
578
+ dp.add_dynamic_vector(
579
+ matrix="sa_matrix",
580
+ interface=Dummy(),
581
+ name="sa-data-vector",
582
+ indices_array=indices_array,
583
+ scale_array=scale_array,
584
+ )
585
+
586
+
587
+ def test_add_dynamic_vector_scale_shapemismatch():
588
+ dp = create_datapackage()
589
+ scale_array = np.array([0.5, 1.0, 2.0, 3.0])
590
+ indices_array = np.array([(1, 4), (2, 5), (3, 6)], dtype=INDICES_DTYPE)
591
+ with pytest.raises(ShapeMismatch):
592
+ dp.add_dynamic_vector(
593
+ matrix="sa_matrix",
594
+ interface=Dummy(),
595
+ name="sa-data-vector",
596
+ indices_array=indices_array,
597
+ scale_array=scale_array,
598
+ )
599
+
600
+
601
+ def test_add_dynamic_array_scale_array():
602
+ dp = create_datapackage()
603
+ scale_array = np.array([0.5, 1.0, 2.0])
604
+ indices_array = np.array([(1, 4), (2, 5), (3, 6)], dtype=INDICES_DTYPE)
605
+ dp.add_dynamic_array(
606
+ matrix="sa_matrix",
607
+ interface=Dummy(),
608
+ name="sa-data-vector",
609
+ indices_array=indices_array,
610
+ scale_array=scale_array,
611
+ )
612
+ assert "sa-data-vector.scale" in [o["name"] for o in dp.resources]
613
+ data, meta = dp.get_resource("sa-data-vector.scale")
614
+ assert meta["kind"] == "scale"
615
+ assert np.allclose(data, scale_array)
616
+
617
+
618
+ def test_add_dynamic_array_scale_dtype():
619
+ dp = create_datapackage()
620
+ scale_array = np.array([1, 2, 3])
621
+ indices_array = np.array([(1, 4), (2, 5), (3, 6)], dtype=INDICES_DTYPE)
622
+ with pytest.raises(WrongDatatype):
623
+ dp.add_dynamic_array(
624
+ matrix="sa_matrix",
625
+ interface=Dummy(),
626
+ name="sa-data-vector",
627
+ indices_array=indices_array,
628
+ scale_array=scale_array,
629
+ )
630
+
631
+
632
+ def test_add_dynamic_array_scale_shapemismatch():
633
+ dp = create_datapackage()
634
+ scale_array = np.array([0.5, 1.0, 2.0, 3.0])
635
+ indices_array = np.array([(1, 4), (2, 5), (3, 6)], dtype=INDICES_DTYPE)
636
+ with pytest.raises(ShapeMismatch):
637
+ dp.add_dynamic_array(
638
+ matrix="sa_matrix",
639
+ interface=Dummy(),
640
+ name="sa-data-vector",
641
+ indices_array=indices_array,
642
+ scale_array=scale_array,
643
+ )
644
+
645
+
646
+ def test_scale_array_parquet_roundtrip(tmp_path):
647
+ scale_array = np.array([0.5, 1.0, 2.0])
648
+ indices_array = np.array([(1, 4), (2, 5), (3, 6)], dtype=INDICES_DTYPE)
649
+ data_array = np.array([100.0, 200.0, 300.0])
650
+
651
+ dp = create_datapackage(
652
+ fs=generic_directory_filesystem(dirpath=tmp_path),
653
+ name="scale-parquet-test",
654
+ )
655
+ dp.add_persistent_vector(
656
+ matrix="sa_matrix",
657
+ data_array=data_array,
658
+ name="sa-data-vector",
659
+ indices_array=indices_array,
660
+ scale_array=scale_array,
661
+ matrix_serialize_format_type=MatrixSerializeFormat.PARQUET,
662
+ )
663
+ dp.finalize_serialization()
664
+
665
+ dp2 = load_datapackage(generic_directory_filesystem(dirpath=tmp_path))
666
+ assert "sa-data-vector.scale" in [o["name"] for o in dp2.resources]
667
+ loaded, meta = dp2.get_resource("sa-data-vector.scale")
668
+ assert meta["kind"] == "scale"
669
+ assert np.allclose(loaded, scale_array)
670
+
671
+
460
672
  def test_simple_graph():
461
673
  data = {
462
674
  "some_matrix": [(1, 2, 3.14), (4, 5, 17, True), (8, 9, 11.11, False)],
File without changes
File without changes
File without changes
File without changes