matrix-utils 0.7.1__tar.gz → 0.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. {matrix_utils-0.7.1 → matrix_utils-0.8}/CHANGELOG.md +9 -0
  2. {matrix_utils-0.7.1 → matrix_utils-0.8}/PKG-INFO +15 -6
  3. {matrix_utils-0.7.1 → matrix_utils-0.8}/README.md +13 -4
  4. {matrix_utils-0.7.1 → matrix_utils-0.8}/matrix_utils/__init__.py +1 -1
  5. {matrix_utils-0.7.1 → matrix_utils-0.8}/matrix_utils/mapped_matrix.py +129 -1
  6. {matrix_utils-0.7.1 → matrix_utils-0.8}/matrix_utils/resource_group.py +79 -0
  7. {matrix_utils-0.7.1 → matrix_utils-0.8}/matrix_utils.egg-info/PKG-INFO +15 -6
  8. {matrix_utils-0.7.1 → matrix_utils-0.8}/matrix_utils.egg-info/requires.txt +1 -1
  9. {matrix_utils-0.7.1 → matrix_utils-0.8}/pyproject.toml +1 -1
  10. {matrix_utils-0.7.1 → matrix_utils-0.8}/tests/mapped_matrix.py +473 -0
  11. {matrix_utils-0.7.1 → matrix_utils-0.8}/tests/monte_carlo.py +2 -2
  12. {matrix_utils-0.7.1 → matrix_utils-0.8}/.gitignore +0 -0
  13. {matrix_utils-0.7.1 → matrix_utils-0.8}/.pre-commit-config.yaml +0 -0
  14. {matrix_utils-0.7.1 → matrix_utils-0.8}/CODE_OF_CONDUCT.md +0 -0
  15. {matrix_utils-0.7.1 → matrix_utils-0.8}/LICENSE +0 -0
  16. {matrix_utils-0.7.1 → matrix_utils-0.8}/MANIFEST.in +0 -0
  17. {matrix_utils-0.7.1 → matrix_utils-0.8}/binder/Brightway 2.5 demonstration.ipynb +0 -0
  18. {matrix_utils-0.7.1 → matrix_utils-0.8}/binder/environment.yml +0 -0
  19. {matrix_utils-0.7.1 → matrix_utils-0.8}/dev/Brightway 2.5 - Linking with IAMs.ipynb +0 -0
  20. {matrix_utils-0.7.1 → matrix_utils-0.8}/dev/Brightway 2.5 demonstration.ipynb +0 -0
  21. {matrix_utils-0.7.1 → matrix_utils-0.8}/dev/Graph traversal with modification.ipynb +0 -0
  22. {matrix_utils-0.7.1 → matrix_utils-0.8}/dev/Indexing speed tests.ipynb +0 -0
  23. {matrix_utils-0.7.1 → matrix_utils-0.8}/dev/Interfaces which produce results for multiple matrices.ipynb +0 -0
  24. {matrix_utils-0.7.1 → matrix_utils-0.8}/dev/Mapping speed tests.ipynb +0 -0
  25. {matrix_utils-0.7.1 → matrix_utils-0.8}/dev/Multifunctionality with Brightway 2.5.ipynb +0 -0
  26. {matrix_utils-0.7.1 → matrix_utils-0.8}/dev/Real-time modification of hybrid databases.ipynb +0 -0
  27. {matrix_utils-0.7.1 → matrix_utils-0.8}/dev/Supply chain modification.ipynb +0 -0
  28. {matrix_utils-0.7.1 → matrix_utils-0.8}/dev/Untitled.ipynb +0 -0
  29. {matrix_utils-0.7.1 → matrix_utils-0.8}/dev/profiling/bw2.prof +0 -0
  30. {matrix_utils-0.7.1 → matrix_utils-0.8}/dev/profiling/bw2.svg +0 -0
  31. {matrix_utils-0.7.1 → matrix_utils-0.8}/dev/profiling/bw25.prof +0 -0
  32. {matrix_utils-0.7.1 → matrix_utils-0.8}/dev/profiling/bw25.svg +0 -0
  33. {matrix_utils-0.7.1 → matrix_utils-0.8}/dev/profiling/bw25_profile.txt +0 -0
  34. {matrix_utils-0.7.1 → matrix_utils-0.8}/dev/profiling/bw25_profiling.py +0 -0
  35. {matrix_utils-0.7.1 → matrix_utils-0.8}/dev/profiling/bw2_profile.txt +0 -0
  36. {matrix_utils-0.7.1 → matrix_utils-0.8}/dev/profiling/bw2_profiling.py +0 -0
  37. {matrix_utils-0.7.1 → matrix_utils-0.8}/dev/profiling/profiling-versus-bw2.py +0 -0
  38. {matrix_utils-0.7.1 → matrix_utils-0.8}/dev/speed_test_unique_indices.py +0 -0
  39. {matrix_utils-0.7.1 → matrix_utils-0.8}/dev/speed_tests.py +0 -0
  40. {matrix_utils-0.7.1 → matrix_utils-0.8}/docs/Makefile +0 -0
  41. {matrix_utils-0.7.1 → matrix_utils-0.8}/docs/conf.py +0 -0
  42. {matrix_utils-0.7.1 → matrix_utils-0.8}/docs/index.rst +0 -0
  43. {matrix_utils-0.7.1 → matrix_utils-0.8}/docs/loading.rst +0 -0
  44. {matrix_utils-0.7.1 → matrix_utils-0.8}/docs/make.bat +0 -0
  45. {matrix_utils-0.7.1 → matrix_utils-0.8}/docs/saving.rst +0 -0
  46. {matrix_utils-0.7.1 → matrix_utils-0.8}/docs/utilities.rst +0 -0
  47. {matrix_utils-0.7.1 → matrix_utils-0.8}/matrix_utils/aggregation.py +0 -0
  48. {matrix_utils-0.7.1 → matrix_utils-0.8}/matrix_utils/array_mapper.py +0 -0
  49. {matrix_utils-0.7.1 → matrix_utils-0.8}/matrix_utils/errors.py +0 -0
  50. {matrix_utils-0.7.1 → matrix_utils-0.8}/matrix_utils/indexers.py +0 -0
  51. {matrix_utils-0.7.1 → matrix_utils-0.8}/matrix_utils/mapped_matrix_dict.py +0 -0
  52. {matrix_utils-0.7.1 → matrix_utils-0.8}/matrix_utils/utils.py +0 -0
  53. {matrix_utils-0.7.1 → matrix_utils-0.8}/matrix_utils.egg-info/SOURCES.txt +0 -0
  54. {matrix_utils-0.7.1 → matrix_utils-0.8}/matrix_utils.egg-info/dependency_links.txt +0 -0
  55. {matrix_utils-0.7.1 → matrix_utils-0.8}/matrix_utils.egg-info/top_level.txt +0 -0
  56. {matrix_utils-0.7.1 → matrix_utils-0.8}/setup.cfg +0 -0
  57. {matrix_utils-0.7.1 → matrix_utils-0.8}/tests/array_mapper.py +0 -0
  58. {matrix_utils-0.7.1 → matrix_utils-0.8}/tests/fixtures/__init__.py +0 -0
  59. {matrix_utils-0.7.1 → matrix_utils-0.8}/tests/fixtures/a-first.zip +0 -0
  60. {matrix_utils-0.7.1 → matrix_utils-0.8}/tests/fixtures/b-second.zip +0 -0
  61. {matrix_utils-0.7.1 → matrix_utils-0.8}/tests/fixtures/basic.py +0 -0
  62. {matrix_utils-0.7.1 → matrix_utils-0.8}/tests/fixtures/create_static_fixtures.py +0 -0
  63. {matrix_utils-0.7.1 → matrix_utils-0.8}/tests/fixtures/sa-1.zip +0 -0
  64. {matrix_utils-0.7.1 → matrix_utils-0.8}/tests/fixtures/sa-2.zip +0 -0
  65. {matrix_utils-0.7.1 → matrix_utils-0.8}/tests/indexers.py +0 -0
  66. {matrix_utils-0.7.1 → matrix_utils-0.8}/tests/integration.py +0 -0
  67. {matrix_utils-0.7.1 → matrix_utils-0.8}/tests/interfaces.py +0 -0
  68. {matrix_utils-0.7.1 → matrix_utils-0.8}/tests/masking.py +0 -0
  69. {matrix_utils-0.7.1 → matrix_utils-0.8}/tests/ordering.py +0 -0
  70. {matrix_utils-0.7.1 → matrix_utils-0.8}/tests/utils.py +0 -0
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.8] - 2026-06-04
4
+
5
+ * Merge [#42](https://github.com/brightway-lca/matrix_utils/pull/42): Rename `scale_array` to `rescale_array`, tracking [bw_processing #104](https://github.com/brightway-lca/bw_processing/pull/104). Adds `ResourceGroup.rescale_current` and `MappedMatrix.input_rescale_vector()`.
6
+ * Merge [#41](https://github.com/brightway-lca/matrix_utils/pull/41): Add `params_array` support from [bw_processing #93](https://github.com/brightway-lca/bw_processing/pull/93). New `ResourceGroup` properties `has_params`, `params_current`, `has_param_labels`, and `param_labels`; new `MappedMatrix.input_params()` method.
7
+ * Merge [#40](https://github.com/brightway-lca/matrix_utils/pull/40): Fix `input_uncertainties` crashing when an array group has no flip array (closes [#8](https://github.com/brightway-lca/matrix_utils/issues/8))
8
+ * Merge [#38](https://github.com/brightway-lca/matrix_utils/pull/38): `NaN` values in data vectors/arrays are skipped during matrix insertion, preserving earlier package values
9
+ * Merge [#37](https://github.com/brightway-lca/matrix_utils/pull/37): Add data access convenience methods (`input_raw_indices`, `input_flip_vector`, `input_provenance`, `n_elements_dropped`, `group`)
10
+ * Merge [#36](https://github.com/brightway-lca/matrix_utils/pull/36): Improve access to indexer values (`indexers`, `local_indexers`, `indexers_by_type`, `indexers_are_unique`)
11
+
3
12
  ### [0.7.1] - 2026-03-20
4
13
 
5
14
  * Fix all elements being masked return sparse matrix instead of array when indexing
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matrix_utils
3
- Version: 0.7.1
3
+ Version: 0.8
4
4
  Summary: Tools to create matrices from data packages
5
5
  Author-email: Chris Mutel <cmutel@gmail.com>
6
6
  Maintainer-email: Chris Mutel <cmutel@gmail.com>
@@ -28,7 +28,7 @@ License-File: LICENSE
28
28
  Requires-Dist: numpy
29
29
  Requires-Dist: scipy>=1.10
30
30
  Requires-Dist: pandas
31
- Requires-Dist: bw_processing>=1.0
31
+ Requires-Dist: bw_processing>=1.5
32
32
  Requires-Dist: stats_arrays
33
33
  Provides-Extra: testing
34
34
  Requires-Dist: matrix_utils; extra == "testing"
@@ -106,21 +106,30 @@ Out[3]:
106
106
 
107
107
  `MappedMatrix` is iterable; calling `next()` will draw new samples from all included stochastic resources, and rebuild the matrix.
108
108
 
109
+ #### Indexers
110
+
111
+ Each datapackage is assigned exactly one indexer, shared across all its resource groups. This indexer determines which column of an array resource is used on each iteration. The design assumption is one indexer per datapackage — `MappedMatrix.indexers` returns a `{name: indexer}` dict at that level. `MappedMatrix.local_indexers` returns a `{group_label: indexer}` dict for what each resource group is actually using, which will normally be the same object (or a `Proxy` wrapping it for combinatorial packages). You may replace `group.indexer` on any individual group after construction, but you are then responsible for the consequences — in particular, `next()` only advances the package-level indexers, so any custom group-level indexer must be advanced manually.
112
+
113
+ #### NaN as a sentinel value
114
+
115
+ A `NaN` value in a data vector or array is treated as **"no data insertion"** — that element is skipped when the matrix is built or rebuilt, leaving the matrix cell at its current value. This is useful for scenario or override packages: place `NaN` at positions you want to inherit from an earlier package, and a real number where you want to override. Non-NaN elements behave normally. `ResourceGroup.data_current` still carries the raw `NaN` so `input_data_vector()` reflects the actual datapackage contents.
116
+
109
117
  You may also find it useful to iterate through `MappedMatrix.groups`, which are instances of `ResourceGroup`, documented below.
110
118
 
111
119
  ### `ResourceGroup` class
112
120
 
113
- A `bw_processing` data package is essentially a metadata file and a bag of data resources. These resources are *grouped*, for multiple resources are needed to build one matrix, or one component of one matrix. For example, one needs not only the data vector, but also the row and column indices to build a simple matrix. One could also have a `flip` vector, in another file, used to flip the signs of data elements before matrix insertion.
121
+ A `bw_processing` data package is essentially a metadata file and a bag of data resources. These resources are *grouped*, for multiple resources are needed to build one matrix, or one component of one matrix. For example, one needs not only the data vector, but also the row and column indices to build a simple matrix. One could also have a `flip` vector (to negate signs before insertion) or a `scale` vector (to apply a per-exchange multiplicative factor before insertion, e.g. for allocation or unit conversion).
114
122
 
115
123
  The `ResourceGroup` class provides a single interface to these data files and their metadata. `ResourceGroup` instances are created automatically by `MappedMatrix`, and available via `MappedMatrix.groups`. The [source code](https://github.com/brightway-lca/matrix_utils/) is pretty readable, and in general you probably don't need to worry about this low-level class, but the following could be useful:
116
124
 
117
125
  * `ResourceGroup.data_original`: The data as it is present in the datapackage, before masking (i.e. the Numpy data vector or array, or the data interface). This is the raw input data, duplicate elements are not aggregated (if applicable).
118
- * `ResourceGroup.data_current`: The data sample used (before aggregation) to build the matrix. It is both masked and flipped.
126
+ * `ResourceGroup.data_current`: The data sample used (before aggregation) to build the matrix. It is masked, flipped (if a flip array is present), and scaled (if a scale array is present).
127
+ * `ResourceGroup.scale`: The scale array with all masks applied. Raises `KeyError` if no scale array is present.
119
128
  * `ResourceGroup.row|col_mapped`: Mapped row and column indices. Has the same length as the datapackage resource, but uses `-1` for values which weren't mapped.
120
129
  * `ResourceGroup.row|col_masked`: The data after the custom filter and mapping mask have been applied.
121
- * `rResourceGroup.row|col_matrix`: Row and column indices (but not data) for insertion into the matrix. These indices are after aggregation within the resource group (if any).
130
+ * `ResourceGroup.row|col_matrix`: Row and column indices (but not data) for insertion into the matrix. These indices are after aggregation within the resource group (if any).
122
131
  * `ResourceGroup.calculate(vector=None)`: Function to recalculate matrix row, column, and data vectors. Uses the current state of the indexers, but re-draws values from data iterators. If `vector` is given, use this instead of the given data source.
123
- * `ResourceGroup.indexer`: The instance of the `Indexer` class applicable for this `ResourceGroup`. Only used for data arrays.
132
+ * `ResourceGroup.indexer`: The instance of the `Indexer` class applicable for this `ResourceGroup`. Only used for data arrays. By design each datapackage has one indexer shared across all its resource groups; this is set up automatically by `MappedMatrix`. You may assign a different indexer to individual groups after construction, but you are then responsible for keeping them consistent (e.g. ensuring they advance together when `next()` is called).
124
133
  * `ResourceGroup.ncols`: The integer number of columns in a data array. Returns `None` if a data vector is present.
125
134
 
126
135
  ## Contributing
@@ -59,21 +59,30 @@ Out[3]:
59
59
 
60
60
  `MappedMatrix` is iterable; calling `next()` will draw new samples from all included stochastic resources, and rebuild the matrix.
61
61
 
62
+ #### Indexers
63
+
64
+ Each datapackage is assigned exactly one indexer, shared across all its resource groups. This indexer determines which column of an array resource is used on each iteration. The design assumption is one indexer per datapackage — `MappedMatrix.indexers` returns a `{name: indexer}` dict at that level. `MappedMatrix.local_indexers` returns a `{group_label: indexer}` dict for what each resource group is actually using, which will normally be the same object (or a `Proxy` wrapping it for combinatorial packages). You may replace `group.indexer` on any individual group after construction, but you are then responsible for the consequences — in particular, `next()` only advances the package-level indexers, so any custom group-level indexer must be advanced manually.
65
+
66
+ #### NaN as a sentinel value
67
+
68
+ A `NaN` value in a data vector or array is treated as **"no data insertion"** — that element is skipped when the matrix is built or rebuilt, leaving the matrix cell at its current value. This is useful for scenario or override packages: place `NaN` at positions you want to inherit from an earlier package, and a real number where you want to override. Non-NaN elements behave normally. `ResourceGroup.data_current` still carries the raw `NaN` so `input_data_vector()` reflects the actual datapackage contents.
69
+
62
70
  You may also find it useful to iterate through `MappedMatrix.groups`, which are instances of `ResourceGroup`, documented below.
63
71
 
64
72
  ### `ResourceGroup` class
65
73
 
66
- A `bw_processing` data package is essentially a metadata file and a bag of data resources. These resources are *grouped*, for multiple resources are needed to build one matrix, or one component of one matrix. For example, one needs not only the data vector, but also the row and column indices to build a simple matrix. One could also have a `flip` vector, in another file, used to flip the signs of data elements before matrix insertion.
74
+ A `bw_processing` data package is essentially a metadata file and a bag of data resources. These resources are *grouped*, for multiple resources are needed to build one matrix, or one component of one matrix. For example, one needs not only the data vector, but also the row and column indices to build a simple matrix. One could also have a `flip` vector (to negate signs before insertion) or a `scale` vector (to apply a per-exchange multiplicative factor before insertion, e.g. for allocation or unit conversion).
67
75
 
68
76
  The `ResourceGroup` class provides a single interface to these data files and their metadata. `ResourceGroup` instances are created automatically by `MappedMatrix`, and available via `MappedMatrix.groups`. The [source code](https://github.com/brightway-lca/matrix_utils/) is pretty readable, and in general you probably don't need to worry about this low-level class, but the following could be useful:
69
77
 
70
78
  * `ResourceGroup.data_original`: The data as it is present in the datapackage, before masking (i.e. the Numpy data vector or array, or the data interface). This is the raw input data, duplicate elements are not aggregated (if applicable).
71
- * `ResourceGroup.data_current`: The data sample used (before aggregation) to build the matrix. It is both masked and flipped.
79
+ * `ResourceGroup.data_current`: The data sample used (before aggregation) to build the matrix. It is masked, flipped (if a flip array is present), and scaled (if a scale array is present).
80
+ * `ResourceGroup.scale`: The scale array with all masks applied. Raises `KeyError` if no scale array is present.
72
81
  * `ResourceGroup.row|col_mapped`: Mapped row and column indices. Has the same length as the datapackage resource, but uses `-1` for values which weren't mapped.
73
82
  * `ResourceGroup.row|col_masked`: The data after the custom filter and mapping mask have been applied.
74
- * `rResourceGroup.row|col_matrix`: Row and column indices (but not data) for insertion into the matrix. These indices are after aggregation within the resource group (if any).
83
+ * `ResourceGroup.row|col_matrix`: Row and column indices (but not data) for insertion into the matrix. These indices are after aggregation within the resource group (if any).
75
84
  * `ResourceGroup.calculate(vector=None)`: Function to recalculate matrix row, column, and data vectors. Uses the current state of the indexers, but re-draws values from data iterators. If `vector` is given, use this instead of the given data source.
76
- * `ResourceGroup.indexer`: The instance of the `Indexer` class applicable for this `ResourceGroup`. Only used for data arrays.
85
+ * `ResourceGroup.indexer`: The instance of the `Indexer` class applicable for this `ResourceGroup`. Only used for data arrays. By design each datapackage has one indexer shared across all its resource groups; this is set up automatically by `MappedMatrix`. You may assign a different indexer to individual groups after construction, but you are then responsible for keeping them consistent (e.g. ensuring they advance together when `next()` is called).
77
86
  * `ResourceGroup.ncols`: The integer number of columns in a data array. Returns `None` if a data vector is present.
78
87
 
79
88
  ## Contributing
@@ -11,7 +11,7 @@ __all__ = (
11
11
  "SparseMatrixDict",
12
12
  )
13
13
 
14
- __version__ = "0.7.1"
14
+ __version__ = "0.8"
15
15
 
16
16
  from matrix_utils.array_mapper import ArrayMapper
17
17
  from matrix_utils.indexers import CombinatorialIndexer, Proxy, RandomIndexer, SequentialIndexer
@@ -219,6 +219,10 @@ class MappedMatrix:
219
219
  self.matrix.data *= 0
220
220
  for group in self.groups:
221
221
  row, col, data = group.calculate()
222
+ # NaN means "no data insertion" — skip those elements so earlier
223
+ # packages' values are preserved rather than overwritten with NaN.
224
+ nan_mask = ~np.isnan(data)
225
+ row, col, data = row[nan_mask], col[nan_mask], data[nan_mask]
222
226
  if group.package.metadata["sum_inter_duplicates"]:
223
227
  self.matrix[row, col] += data
224
228
  else:
@@ -256,6 +260,60 @@ class MappedMatrix:
256
260
  for obj in resources:
257
261
  obj.add_indexer(indexer=package.indexer)
258
262
 
263
+ @property
264
+ def indexers(self) -> dict:
265
+ """Return package-level indexers keyed by datapackage name.
266
+
267
+ By design each datapackage has one indexer shared across all its resource groups,
268
+ but this constraint is not enforced — individual groups can be given different
269
+ indexers after construction. Use ``local_indexers`` to inspect what each group
270
+ is actually using.
271
+ """
272
+ return {
273
+ package.metadata["name"]: package.indexer
274
+ for package in self.packages
275
+ if hasattr(package, "indexer")
276
+ }
277
+
278
+ @property
279
+ def local_indexers(self) -> dict:
280
+ """Return the indexer actually used by each resource group, keyed by group label.
281
+
282
+ Normally all groups within a package share the same indexer (or a ``Proxy``
283
+ wrapping it for combinatorial packages), but users can assign a different indexer
284
+ to any group after construction, so this may differ from ``indexers``.
285
+ """
286
+ return {group.label: group.indexer for group in self.groups if hasattr(group, "indexer")}
287
+
288
+ def indexers_by_type(self, indexer_type: type) -> list:
289
+ """Return all package-level indexers that are instances of ``indexer_type``."""
290
+ return [indexer for indexer in self.indexers.values() if isinstance(indexer, indexer_type)]
291
+
292
+ @property
293
+ def indexers_are_unique(self) -> bool:
294
+ """True if no two packages share the same indexer instance."""
295
+ indexers = list(self.indexers.values())
296
+ return len({id(i) for i in indexers}) == len(indexers)
297
+
298
+ def group(self, label: str) -> "ResourceGroup":
299
+ """Return the ResourceGroup with the given label.
300
+
301
+ Raises ``KeyError`` if no group with that label exists.
302
+ """
303
+ for grp in self.groups:
304
+ if grp.label == label:
305
+ return grp
306
+ raise KeyError(label)
307
+
308
+ @property
309
+ def n_elements_dropped(self) -> int:
310
+ """Total number of datapackage elements dropped across all resource groups.
311
+
312
+ An element is dropped when it is excluded by the custom filter or when its
313
+ row/column id could not be mapped to a matrix index.
314
+ """
315
+ return sum(grp.n_elements_dropped for grp in self.groups)
316
+
259
317
  def input_data_vector(self) -> np.ndarray:
260
318
  return np.hstack([group.data_current for group in self.groups])
261
319
 
@@ -273,6 +331,52 @@ class MappedMatrix:
273
331
  array["col"] = cols
274
332
  return array
275
333
 
334
+ def input_raw_indices(self) -> np.ndarray:
335
+ """Return original datapackage indices (before mapping), with masks applied.
336
+
337
+ The result has the same length and order as ``input_data_vector`` and
338
+ ``input_row_col_indices``, but contains the raw source ids from the datapackage
339
+ rather than the translated matrix row/column positions. Useful for tracing which
340
+ original ids contributed to each matrix element.
341
+ """
342
+ arrays = []
343
+ for grp in self.groups:
344
+ if grp.empty:
345
+ arrays.append(np.array([], dtype=INDICES_DTYPE))
346
+ else:
347
+ arrays.append(grp.apply_masks(grp.get_indices_data()))
348
+ return np.concatenate(arrays)
349
+
350
+ def input_rescale_vector(self) -> np.ndarray:
351
+ """Return rescale factors for each element, aligned with ``input_data_vector``.
352
+
353
+ Groups without a rescale array contribute all-ones values.
354
+ """
355
+ arrays = []
356
+ for grp in self.groups:
357
+ if grp.empty:
358
+ arrays.append(np.array([]))
359
+ else:
360
+ rc = grp.rescale_current
361
+ arrays.append(rc if rc is not None else np.ones(len(grp.data_current)))
362
+ return np.hstack(arrays)
363
+
364
+ def input_flip_vector(self) -> np.ndarray:
365
+ """Return a boolean array indicating which elements were negated before matrix insertion.
366
+
367
+ Groups without a flip array contribute all-``False`` values. The result aligns
368
+ element-wise with ``input_data_vector`` and ``input_row_col_indices``.
369
+ """
370
+ arrays = []
371
+ for grp in self.groups:
372
+ if grp.empty:
373
+ arrays.append(np.array([], dtype=bool))
374
+ elif grp.has_flip:
375
+ arrays.append(grp.flip)
376
+ else:
377
+ arrays.append(np.zeros(len(grp.data_current), dtype=bool))
378
+ return np.hstack(arrays)
379
+
276
380
  def input_provenance(self) -> List[tuple]:
277
381
  """Describe where the data in the other ``input_X`` comes from. Returns a list
278
382
  of ``(datapackage, group_label, (start_index, end_index))`` tuples.
@@ -293,6 +397,29 @@ class MappedMatrix:
293
397
  position += num_elements
294
398
  return result
295
399
 
400
+ def input_params(self) -> dict:
401
+ """Return current parameter values keyed by ``(package, group_label)``.
402
+
403
+ Only groups that carry a params array are included; groups without
404
+ params are omitted entirely (not present vs. ``None``-valued).
405
+ Keying by ``(package, group_label)`` avoids silent collisions when
406
+ two packages contribute groups with the same label.
407
+
408
+ For vector groups the full 1-D params array is returned (parameters
409
+ are fixed and do not vary by iteration). For array groups the column
410
+ matching the current indexer position is returned, so the result
411
+ stays in sync with ``input_data_vector`` across Monte Carlo
412
+ iterations.
413
+ """
414
+ result = {}
415
+ for package, groups in self.packages.items():
416
+ for group in groups:
417
+ try:
418
+ result[(package, group.label)] = group.params_current
419
+ except KeyError:
420
+ pass
421
+ return result
422
+
296
423
  def input_indexer_vector(self) -> np.ndarray:
297
424
  index_values = []
298
425
 
@@ -363,7 +490,8 @@ class MappedMatrix:
363
490
 
364
491
  array = self._construct_distributions_array(group.data_current, uncertainty_type=98)
365
492
  array["loc"] = np.mean(data, axis=1)
366
- array["loc"][group.flip] *= -1
493
+ if group.has_flip:
494
+ array["loc"][group.flip] *= -1
367
495
  array["scale"] = np.std(data, axis=1)
368
496
  arrays.append(array)
369
497
  elif group.is_interface():
@@ -119,6 +119,68 @@ class ResourceGroup:
119
119
  except KeyError:
120
120
  return False
121
121
 
122
+ @property
123
+ def has_flip(self) -> bool:
124
+ try:
125
+ self.get_resource_by_suffix("flip")
126
+ return True
127
+ except KeyError:
128
+ return False
129
+
130
+ @property
131
+ def has_rescale(self) -> bool:
132
+ try:
133
+ self.get_resource_by_suffix("rescale")
134
+ return True
135
+ except KeyError:
136
+ return False
137
+
138
+ @property
139
+ def has_params(self) -> bool:
140
+ try:
141
+ self.get_resource_by_suffix("params")
142
+ return True
143
+ except KeyError:
144
+ return False
145
+
146
+ @property
147
+ def has_param_labels(self) -> bool:
148
+ try:
149
+ self.get_resource_by_suffix("param_labels")
150
+ return True
151
+ except KeyError:
152
+ return False
153
+
154
+ @property
155
+ def params_current(self) -> np.ndarray:
156
+ """Current parameter values.
157
+
158
+ For vectors: returns the full 1-D params array (parameters are fixed
159
+ and do not vary by indexer position).
160
+ For arrays: returns the column at the current indexer position,
161
+ mirroring how ``calculate()`` picks the active data column.
162
+
163
+ Raises ``KeyError`` if no params array exists for this group.
164
+ """
165
+ data = self.get_resource_by_suffix("params")
166
+ if self.vector:
167
+ return data
168
+ return data[:, self.indexer.index % data.shape[1]]
169
+
170
+ @property
171
+ def param_labels(self) -> dict:
172
+ """Raw param-labels dict. Always has ``"values"``;
173
+ also has ``"schema"`` when a label schema was provided at write time.
174
+
175
+ Raises ``KeyError`` if no param_labels resource exists.
176
+ """
177
+ return self.get_resource_by_suffix("param_labels")
178
+
179
+ @property
180
+ def n_elements_dropped(self) -> int:
181
+ """Number of original datapackage elements dropped by the custom filter and mapping mask."""
182
+ return len(self.get_indices_data()) - len(self.row_masked)
183
+
122
184
  @property
123
185
  def data_original(self):
124
186
  if self.use_distributions and self.has_distributions:
@@ -131,6 +193,18 @@ class ResourceGroup:
131
193
  """The flip array, with all masks applied (if necessary)."""
132
194
  return self.apply_masks(self.get_resource_by_suffix("flip"))
133
195
 
196
+ @property
197
+ def rescale(self):
198
+ """The rescale array, with all masks applied (if necessary)."""
199
+ return self.apply_masks(self.get_resource_by_suffix("rescale"))
200
+
201
+ @property
202
+ def rescale_current(self) -> Optional[np.ndarray]:
203
+ """Current rescale values with masks applied, or ``None`` if no rescale array exists."""
204
+ if self.has_rescale:
205
+ return self.rescale
206
+ return None
207
+
134
208
  def get_indices_data(self):
135
209
  """The source data for the indices array."""
136
210
  indices = self.get_resource_by_suffix("indices")
@@ -299,6 +373,11 @@ class ResourceGroup:
299
373
  # No flip array
300
374
  pass
301
375
 
376
+ try:
377
+ data *= self.rescale
378
+ except KeyError:
379
+ pass
380
+
302
381
  # Second copy because we want to store the data before aggregation
303
382
  self.data_current = data.copy()
304
383
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matrix_utils
3
- Version: 0.7.1
3
+ Version: 0.8
4
4
  Summary: Tools to create matrices from data packages
5
5
  Author-email: Chris Mutel <cmutel@gmail.com>
6
6
  Maintainer-email: Chris Mutel <cmutel@gmail.com>
@@ -28,7 +28,7 @@ License-File: LICENSE
28
28
  Requires-Dist: numpy
29
29
  Requires-Dist: scipy>=1.10
30
30
  Requires-Dist: pandas
31
- Requires-Dist: bw_processing>=1.0
31
+ Requires-Dist: bw_processing>=1.5
32
32
  Requires-Dist: stats_arrays
33
33
  Provides-Extra: testing
34
34
  Requires-Dist: matrix_utils; extra == "testing"
@@ -106,21 +106,30 @@ Out[3]:
106
106
 
107
107
  `MappedMatrix` is iterable; calling `next()` will draw new samples from all included stochastic resources, and rebuild the matrix.
108
108
 
109
+ #### Indexers
110
+
111
+ Each datapackage is assigned exactly one indexer, shared across all its resource groups. This indexer determines which column of an array resource is used on each iteration. The design assumption is one indexer per datapackage — `MappedMatrix.indexers` returns a `{name: indexer}` dict at that level. `MappedMatrix.local_indexers` returns a `{group_label: indexer}` dict for what each resource group is actually using, which will normally be the same object (or a `Proxy` wrapping it for combinatorial packages). You may replace `group.indexer` on any individual group after construction, but you are then responsible for the consequences — in particular, `next()` only advances the package-level indexers, so any custom group-level indexer must be advanced manually.
112
+
113
+ #### NaN as a sentinel value
114
+
115
+ A `NaN` value in a data vector or array is treated as **"no data insertion"** — that element is skipped when the matrix is built or rebuilt, leaving the matrix cell at its current value. This is useful for scenario or override packages: place `NaN` at positions you want to inherit from an earlier package, and a real number where you want to override. Non-NaN elements behave normally. `ResourceGroup.data_current` still carries the raw `NaN` so `input_data_vector()` reflects the actual datapackage contents.
116
+
109
117
  You may also find it useful to iterate through `MappedMatrix.groups`, which are instances of `ResourceGroup`, documented below.
110
118
 
111
119
  ### `ResourceGroup` class
112
120
 
113
- A `bw_processing` data package is essentially a metadata file and a bag of data resources. These resources are *grouped*, for multiple resources are needed to build one matrix, or one component of one matrix. For example, one needs not only the data vector, but also the row and column indices to build a simple matrix. One could also have a `flip` vector, in another file, used to flip the signs of data elements before matrix insertion.
121
+ A `bw_processing` data package is essentially a metadata file and a bag of data resources. These resources are *grouped*, for multiple resources are needed to build one matrix, or one component of one matrix. For example, one needs not only the data vector, but also the row and column indices to build a simple matrix. One could also have a `flip` vector (to negate signs before insertion) or a `scale` vector (to apply a per-exchange multiplicative factor before insertion, e.g. for allocation or unit conversion).
114
122
 
115
123
  The `ResourceGroup` class provides a single interface to these data files and their metadata. `ResourceGroup` instances are created automatically by `MappedMatrix`, and available via `MappedMatrix.groups`. The [source code](https://github.com/brightway-lca/matrix_utils/) is pretty readable, and in general you probably don't need to worry about this low-level class, but the following could be useful:
116
124
 
117
125
  * `ResourceGroup.data_original`: The data as it is present in the datapackage, before masking (i.e. the Numpy data vector or array, or the data interface). This is the raw input data, duplicate elements are not aggregated (if applicable).
118
- * `ResourceGroup.data_current`: The data sample used (before aggregation) to build the matrix. It is both masked and flipped.
126
+ * `ResourceGroup.data_current`: The data sample used (before aggregation) to build the matrix. It is masked, flipped (if a flip array is present), and scaled (if a scale array is present).
127
+ * `ResourceGroup.scale`: The scale array with all masks applied. Raises `KeyError` if no scale array is present.
119
128
  * `ResourceGroup.row|col_mapped`: Mapped row and column indices. Has the same length as the datapackage resource, but uses `-1` for values which weren't mapped.
120
129
  * `ResourceGroup.row|col_masked`: The data after the custom filter and mapping mask have been applied.
121
- * `rResourceGroup.row|col_matrix`: Row and column indices (but not data) for insertion into the matrix. These indices are after aggregation within the resource group (if any).
130
+ * `ResourceGroup.row|col_matrix`: Row and column indices (but not data) for insertion into the matrix. These indices are after aggregation within the resource group (if any).
122
131
  * `ResourceGroup.calculate(vector=None)`: Function to recalculate matrix row, column, and data vectors. Uses the current state of the indexers, but re-draws values from data iterators. If `vector` is given, use this instead of the given data source.
123
- * `ResourceGroup.indexer`: The instance of the `Indexer` class applicable for this `ResourceGroup`. Only used for data arrays.
132
+ * `ResourceGroup.indexer`: The instance of the `Indexer` class applicable for this `ResourceGroup`. Only used for data arrays. By design each datapackage has one indexer shared across all its resource groups; this is set up automatically by `MappedMatrix`. You may assign a different indexer to individual groups after construction, but you are then responsible for keeping them consistent (e.g. ensuring they advance together when `next()` is called).
124
133
  * `ResourceGroup.ncols`: The integer number of columns in a data array. Returns `None` if a data vector is present.
125
134
 
126
135
  ## Contributing
@@ -1,7 +1,7 @@
1
1
  numpy
2
2
  scipy>=1.10
3
3
  pandas
4
- bw_processing>=1.0
4
+ bw_processing>=1.5
5
5
  stats_arrays
6
6
 
7
7
  [dev]
@@ -39,7 +39,7 @@ dependencies = [
39
39
  "numpy",
40
40
  "scipy>=1.10",
41
41
  "pandas",
42
- "bw_processing>=1.0",
42
+ "bw_processing>=1.5",
43
43
  "stats_arrays",
44
44
  ]
45
45
 
@@ -9,6 +9,108 @@ from fsspec.implementations.zip import ZipFileSystem
9
9
  from matrix_utils import ArrayMapper, MappedMatrix
10
10
  from matrix_utils.errors import AllArraysEmpty, EmptyArray
11
11
 
12
+ # --- input_params ---
13
+
14
+
15
+ def _mm_with_params(**kwargs):
16
+ """Build a MappedMatrix from the given datapackages."""
17
+ return MappedMatrix(
18
+ matrix="foo",
19
+ use_vectors=True,
20
+ use_arrays=True,
21
+ use_distributions=False,
22
+ **kwargs,
23
+ )
24
+
25
+
26
+ def test_input_params_empty_when_no_params():
27
+ mm = _mm_with_params(packages=[basic_mm()])
28
+ assert mm.input_params() == {}
29
+
30
+
31
+ def test_input_params_vector():
32
+ dp = bwp.create_datapackage()
33
+ params = np.array([0.1, 0.9])
34
+ dp.add_persistent_vector(
35
+ matrix="foo",
36
+ name="v",
37
+ indices_array=np.array([(0, 0), (1, 1)], dtype=bwp.INDICES_DTYPE),
38
+ data_array=np.array([5.0, 7.0]),
39
+ params_array=params,
40
+ )
41
+ mm = _mm_with_params(packages=[dp])
42
+ result = mm.input_params()
43
+ pkg = list(mm.packages.keys())[0]
44
+ assert list(result.keys()) == [(pkg, "v")]
45
+ assert np.allclose(result[(pkg, "v")], params)
46
+
47
+
48
+ def test_input_params_only_groups_with_params_included():
49
+ dp = bwp.create_datapackage()
50
+ dp.add_persistent_vector(
51
+ matrix="foo",
52
+ name="with_params",
53
+ indices_array=np.array([(0, 0)], dtype=bwp.INDICES_DTYPE),
54
+ data_array=np.array([1.0]),
55
+ params_array=np.array([42.0]),
56
+ )
57
+ dp.add_persistent_vector(
58
+ matrix="foo",
59
+ name="no_params",
60
+ indices_array=np.array([(1, 1)], dtype=bwp.INDICES_DTYPE),
61
+ data_array=np.array([2.0]),
62
+ )
63
+ mm = _mm_with_params(packages=[dp])
64
+ result = mm.input_params()
65
+ labels = {label for (_, label) in result}
66
+ assert labels == {"with_params"}
67
+
68
+
69
+ def test_input_params_array_tracks_indexer():
70
+ dp = bwp.create_datapackage(sequential=True)
71
+ data = np.array([[10.0, 11.0, 12.0], [20.0, 21.0, 22.0]])
72
+ params = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
73
+ dp.add_persistent_array(
74
+ matrix="foo",
75
+ name="a",
76
+ indices_array=np.array([(0, 0), (1, 1)], dtype=bwp.INDICES_DTYPE),
77
+ data_array=data,
78
+ params_array=params,
79
+ )
80
+ mm = _mm_with_params(packages=[dp])
81
+ pkg = list(mm.packages.keys())[0]
82
+ assert np.allclose(mm.input_params()[(pkg, "a")], [1.0, 4.0])
83
+ next(mm)
84
+ assert np.allclose(mm.input_params()[(pkg, "a")], [2.0, 5.0])
85
+ next(mm)
86
+ assert np.allclose(mm.input_params()[(pkg, "a")], [3.0, 6.0])
87
+
88
+
89
+ def test_input_params_duplicate_labels_across_packages():
90
+ dp1 = bwp.create_datapackage()
91
+ dp1.add_persistent_vector(
92
+ matrix="foo",
93
+ name="v",
94
+ indices_array=np.array([(0, 0)], dtype=bwp.INDICES_DTYPE),
95
+ data_array=np.array([1.0]),
96
+ params_array=np.array([0.1]),
97
+ )
98
+ dp2 = bwp.create_datapackage()
99
+ dp2.add_persistent_vector(
100
+ matrix="foo",
101
+ name="v",
102
+ indices_array=np.array([(1, 1)], dtype=bwp.INDICES_DTYPE),
103
+ data_array=np.array([2.0]),
104
+ params_array=np.array([0.9]),
105
+ )
106
+ mm = _mm_with_params(packages=[dp1, dp2])
107
+ result = mm.input_params()
108
+ assert len(result) == 2
109
+ values = list(result.values())
110
+ assert any(np.allclose(v, [0.1]) for v in values)
111
+ assert any(np.allclose(v, [0.9]) for v in values)
112
+
113
+
12
114
  dirpath = Path(__file__).parent.resolve() / "fixtures"
13
115
 
14
116
 
@@ -708,6 +810,29 @@ def test_input_uncertainties_no_distributions(sensitivity_dps):
708
810
  assert np.allclose(ua[field], expected[field], equal_nan=True)
709
811
 
710
812
 
813
+ def test_input_uncertainties_array_group_without_flip():
814
+ """Regression test for issue #8: array group without flip should not crash."""
815
+ dp = bwp.create_datapackage()
816
+ indices_array = np.array([(10, 10), (11, 11)], dtype=bwp.INDICES_DTYPE)
817
+ data_array = np.array([[7, 8], [8, 9], [9, 10]]).T
818
+ dp.add_persistent_array(
819
+ matrix="matrix",
820
+ data_array=data_array,
821
+ name="no_flip",
822
+ indices_array=indices_array,
823
+ )
824
+ mm = MappedMatrix(
825
+ packages=[dp],
826
+ matrix="matrix",
827
+ use_vectors=True,
828
+ use_arrays=True,
829
+ use_distributions=False,
830
+ )
831
+ ua = mm.input_uncertainties()
832
+ assert ua.shape == (2,)
833
+ assert np.allclose(ua["loc"], [8.0, 9.0])
834
+
835
+
711
836
  def test_input_index_vector(sensitivity_dps):
712
837
  dp = bwp.create_datapackage(combinatorial=True)
713
838
 
@@ -868,3 +993,351 @@ def test_input_indexer_vector_raises_on_unsupported_type():
868
993
 
869
994
  with pytest.raises(ValueError, match="Can't understand indexer value"):
870
995
  mm.input_indexer_vector()
996
+
997
+
998
+ def test_indexers_single_package():
999
+ from matrix_utils.indexers import RandomIndexer
1000
+
1001
+ dp = basic_mm(name="alpha")
1002
+ mm = MappedMatrix(packages=[dp], matrix="foo", use_arrays=False, use_distributions=False)
1003
+ gi = mm.indexers
1004
+ assert list(gi.keys()) == ["alpha"]
1005
+ assert isinstance(gi["alpha"], RandomIndexer)
1006
+
1007
+
1008
+ def test_indexers_multiple_packages():
1009
+ from matrix_utils.indexers import RandomIndexer
1010
+
1011
+ dp1 = basic_mm(name="pkg-one")
1012
+ dp2 = basic_mm(name="pkg-two")
1013
+ mm = MappedMatrix(packages=[dp1, dp2], matrix="foo", use_arrays=False, use_distributions=False)
1014
+ gi = mm.indexers
1015
+ assert set(gi.keys()) == {"pkg-one", "pkg-two"}
1016
+ assert isinstance(gi["pkg-one"], RandomIndexer)
1017
+ assert isinstance(gi["pkg-two"], RandomIndexer)
1018
+ # each package gets its own indexer instance
1019
+ assert gi["pkg-one"] is not gi["pkg-two"]
1020
+
1021
+
1022
+ def test_local_indexers_keyed_by_group_label():
1023
+ from matrix_utils.indexers import RandomIndexer
1024
+
1025
+ dp = basic_mm(name="alpha")
1026
+ mm = MappedMatrix(packages=[dp], matrix="foo", use_arrays=False, use_distributions=False)
1027
+ li = mm.local_indexers
1028
+ # basic_mm adds two vector groups: "vector" and "vector2"
1029
+ assert set(li.keys()) == {"vector", "vector2"}
1030
+ # both groups share the same package-level indexer instance
1031
+ assert isinstance(li["vector"], RandomIndexer)
1032
+ assert li["vector"] is li["vector2"]
1033
+
1034
+
1035
+ def test_local_indexers_combinatorial_are_proxies():
1036
+ from matrix_utils.indexers import CombinatorialIndexer, Proxy
1037
+
1038
+ dp = bwp.create_datapackage(combinatorial=True, name="combo")
1039
+ indices = np.array([(0, 0), (1, 1)], dtype=bwp.INDICES_DTYPE)
1040
+ dp.add_persistent_array(
1041
+ matrix="foo", name="g", indices_array=indices, data_array=np.array([[1, 2], [3, 4]]).T
1042
+ )
1043
+ dp.add_persistent_array(
1044
+ matrix="foo", name="h", indices_array=indices, data_array=np.array([[5, 6], [7, 8]]).T
1045
+ )
1046
+
1047
+ mm = MappedMatrix(packages=[dp], matrix="foo")
1048
+ li = mm.local_indexers
1049
+ assert set(li.keys()) == {"g", "h"}
1050
+ assert all(isinstance(v, Proxy) for v in li.values())
1051
+ # global indexer is the underlying CombinatorialIndexer
1052
+ assert isinstance(mm.indexers["combo"], CombinatorialIndexer)
1053
+
1054
+
1055
+ def test_indexers_by_type():
1056
+ from matrix_utils.indexers import RandomIndexer, SequentialIndexer
1057
+
1058
+ dp_rand = basic_mm(name="rand")
1059
+ dp_seq = bwp.create_datapackage(sequential=True, name="seq")
1060
+ dp_seq.add_persistent_vector(
1061
+ matrix="foo",
1062
+ name="v",
1063
+ indices_array=np.array([(0, 0)], dtype=bwp.INDICES_DTYPE),
1064
+ data_array=np.array([1.0]),
1065
+ )
1066
+ mm = MappedMatrix(
1067
+ packages=[dp_rand, dp_seq], matrix="foo", use_arrays=False, use_distributions=False
1068
+ )
1069
+
1070
+ rand_indexers = mm.indexers_by_type(RandomIndexer)
1071
+ seq_indexers = mm.indexers_by_type(SequentialIndexer)
1072
+ assert len(rand_indexers) == 1
1073
+ assert isinstance(rand_indexers[0], RandomIndexer)
1074
+ assert len(seq_indexers) == 1
1075
+ assert isinstance(seq_indexers[0], SequentialIndexer)
1076
+
1077
+
1078
+ def test_indexers_are_unique_true():
1079
+ dp1 = basic_mm(name="a")
1080
+ dp2 = basic_mm(name="b")
1081
+ mm = MappedMatrix(packages=[dp1, dp2], matrix="foo", use_arrays=False, use_distributions=False)
1082
+ assert mm.indexers_are_unique is True
1083
+
1084
+
1085
+ def test_indexers_are_unique_false_with_override():
1086
+ from matrix_utils.indexers import SequentialIndexer
1087
+
1088
+ shared = SequentialIndexer()
1089
+ dp1 = basic_mm(name="a")
1090
+ dp2 = basic_mm(name="b")
1091
+ mm = MappedMatrix(
1092
+ packages=[dp1, dp2],
1093
+ matrix="foo",
1094
+ use_arrays=False,
1095
+ use_distributions=False,
1096
+ indexer_override=shared,
1097
+ )
1098
+ assert mm.indexers_are_unique is False
1099
+
1100
+
1101
+ # ── group() ───────────────────────────────────────────────────────────────────
1102
+
1103
+
1104
+ def test_group_lookup_found():
1105
+ mm = MappedMatrix(
1106
+ packages=[basic_mm()], matrix="foo", use_arrays=False, use_distributions=False
1107
+ )
1108
+ grp = mm.group("vector")
1109
+ assert grp.label == "vector"
1110
+
1111
+
1112
+ def test_group_lookup_not_found():
1113
+ mm = MappedMatrix(
1114
+ packages=[basic_mm()], matrix="foo", use_arrays=False, use_distributions=False
1115
+ )
1116
+ with pytest.raises(KeyError):
1117
+ mm.group("nonexistent")
1118
+
1119
+
1120
+ # ── has_flip / has_rescale ────────────────────────────────────────────────────
1121
+
1122
+
1123
+ def test_has_flip_true():
1124
+ mm = MappedMatrix(
1125
+ packages=[diagonal()], matrix="foo", use_arrays=False, use_distributions=False
1126
+ )
1127
+ assert mm.group("vector").has_flip is True
1128
+
1129
+
1130
+ def test_has_flip_false():
1131
+ mm = MappedMatrix(
1132
+ packages=[basic_mm()], matrix="foo", use_arrays=False, use_distributions=False
1133
+ )
1134
+ assert mm.group("vector").has_flip is False
1135
+
1136
+
1137
+ def test_has_rescale_false():
1138
+ mm = MappedMatrix(
1139
+ packages=[basic_mm()], matrix="foo", use_arrays=False, use_distributions=False
1140
+ )
1141
+ assert mm.group("vector").has_rescale is False
1142
+
1143
+
1144
+ def test_has_rescale_true():
1145
+ dp = bwp.create_datapackage()
1146
+ dp.add_persistent_vector(
1147
+ matrix="foo",
1148
+ name="sv",
1149
+ indices_array=np.array([(0, 0), (1, 1)], dtype=bwp.INDICES_DTYPE),
1150
+ data_array=np.array([1.0, 2.0]),
1151
+ rescale_array=np.array([0.5, 2.0]),
1152
+ )
1153
+ mm = MappedMatrix(packages=[dp], matrix="foo", use_arrays=False, use_distributions=False)
1154
+ assert mm.group("sv").has_rescale is True
1155
+
1156
+
1157
+ # ── n_elements_dropped ─────────────────────────────────────────────────────────────────
1158
+
1159
+
1160
+ def test_n_elements_dropped_none():
1161
+ mm = MappedMatrix(
1162
+ packages=[basic_mm()], matrix="foo", use_arrays=False, use_distributions=False
1163
+ )
1164
+ assert mm.n_elements_dropped == 0
1165
+ assert mm.group("vector").n_elements_dropped == 0
1166
+
1167
+
1168
+ def test_n_elements_dropped_custom_filter():
1169
+ mm = MappedMatrix(
1170
+ packages=[basic_mm()],
1171
+ matrix="foo",
1172
+ use_arrays=False,
1173
+ use_distributions=False,
1174
+ custom_filter=lambda x: x["row"] < 5,
1175
+ )
1176
+ # basic_mm vector has rows [0, 2, 4, 8]; filter keeps [0, 2, 4], drops 8
1177
+ assert mm.group("vector").n_elements_dropped == 1
1178
+
1179
+
1180
+ def test_n_elements_dropped_unmapped():
1181
+ # Build a matrix with a pre-existing row_mapper that excludes some ids
1182
+ dp = bwp.create_datapackage()
1183
+ dp.add_persistent_vector(
1184
+ matrix="foo",
1185
+ name="v",
1186
+ indices_array=np.array([(0, 0), (99, 0)], dtype=bwp.INDICES_DTYPE),
1187
+ data_array=np.array([1.0, 2.0]),
1188
+ )
1189
+ # row_mapper that only knows about id 0, not 99
1190
+ am = ArrayMapper(array=np.array([0]))
1191
+ mm = MappedMatrix(
1192
+ packages=[dp], matrix="foo", use_arrays=False, use_distributions=False, row_mapper=am
1193
+ )
1194
+ assert mm.group("v").n_elements_dropped == 1
1195
+ assert mm.n_elements_dropped == 1
1196
+
1197
+
1198
+ def test_n_elements_dropped_total():
1199
+ dp1 = bwp.create_datapackage()
1200
+ dp1.add_persistent_vector(
1201
+ matrix="foo",
1202
+ name="a",
1203
+ indices_array=np.array([(0, 0), (99, 0)], dtype=bwp.INDICES_DTYPE),
1204
+ data_array=np.array([1.0, 2.0]),
1205
+ )
1206
+ dp2 = bwp.create_datapackage()
1207
+ dp2.add_persistent_vector(
1208
+ matrix="foo",
1209
+ name="b",
1210
+ indices_array=np.array([(0, 0), (98, 0)], dtype=bwp.INDICES_DTYPE),
1211
+ data_array=np.array([3.0, 4.0]),
1212
+ )
1213
+ am = ArrayMapper(array=np.array([0]))
1214
+ mm = MappedMatrix(
1215
+ packages=[dp1, dp2], matrix="foo", use_arrays=False, use_distributions=False, row_mapper=am
1216
+ )
1217
+ assert mm.n_elements_dropped == 2
1218
+
1219
+
1220
+ # ── input_raw_indices() ───────────────────────────────────────────────────────
1221
+
1222
+
1223
+ def test_input_raw_indices_aligns_with_data_vector():
1224
+ mm = MappedMatrix(
1225
+ packages=[basic_mm()], matrix="foo", use_arrays=False, use_distributions=False
1226
+ )
1227
+ raw = mm.input_raw_indices()
1228
+ data = mm.input_data_vector()
1229
+ assert len(raw) == len(data)
1230
+
1231
+
1232
+ def test_input_raw_indices_contains_original_ids():
1233
+ dp = bwp.create_datapackage()
1234
+ dp.add_persistent_vector(
1235
+ matrix="foo",
1236
+ name="v",
1237
+ indices_array=np.array([(10, 20), (30, 40)], dtype=bwp.INDICES_DTYPE),
1238
+ data_array=np.array([1.0, 2.0]),
1239
+ )
1240
+ mm = MappedMatrix(packages=[dp], matrix="foo", use_arrays=False, use_distributions=False)
1241
+ raw = mm.input_raw_indices()
1242
+ assert list(raw["row"]) == [10, 30]
1243
+ assert list(raw["col"]) == [20, 40]
1244
+
1245
+
1246
+ def test_input_raw_indices_excludes_dropped():
1247
+ dp = bwp.create_datapackage()
1248
+ dp.add_persistent_vector(
1249
+ matrix="foo",
1250
+ name="v",
1251
+ indices_array=np.array([(0, 0), (99, 0)], dtype=bwp.INDICES_DTYPE),
1252
+ data_array=np.array([1.0, 2.0]),
1253
+ )
1254
+ am = ArrayMapper(array=np.array([0]))
1255
+ mm = MappedMatrix(
1256
+ packages=[dp], matrix="foo", use_arrays=False, use_distributions=False, row_mapper=am
1257
+ )
1258
+ raw = mm.input_raw_indices()
1259
+ # id 99 was unmapped, so only id 0 survives
1260
+ assert len(raw) == 1
1261
+ assert raw["row"][0] == 0
1262
+
1263
+
1264
+ # ── input_flip_vector() ───────────────────────────────────────────────────────
1265
+
1266
+
1267
+ def test_input_flip_vector_no_flip():
1268
+ mm = MappedMatrix(
1269
+ packages=[basic_mm()], matrix="foo", use_arrays=False, use_distributions=False
1270
+ )
1271
+ flip = mm.input_flip_vector()
1272
+ assert flip.dtype == bool
1273
+ assert not flip.any()
1274
+ assert len(flip) == len(mm.input_data_vector())
1275
+
1276
+
1277
+ def test_input_flip_vector_with_flip():
1278
+ mm = MappedMatrix(
1279
+ packages=[diagonal()], matrix="foo", use_arrays=False, use_distributions=False
1280
+ )
1281
+ flip = mm.input_flip_vector()
1282
+ # diagonal fixture: flip_array = [False, True, False, False]
1283
+ assert flip.dtype == bool
1284
+ assert flip.sum() == 1
1285
+ assert len(flip) == len(mm.input_data_vector())
1286
+
1287
+
1288
+ def test_nan_in_vector_skips_insertion():
1289
+ dp = bwp.create_datapackage()
1290
+ dp.add_persistent_vector(
1291
+ matrix="foo",
1292
+ name="v",
1293
+ indices_array=np.array([(0, 0), (1, 1), (2, 2)], dtype=bwp.INDICES_DTYPE),
1294
+ data_array=np.array([1.0, np.nan, 3.0]),
1295
+ )
1296
+ mm = MappedMatrix(packages=[dp], matrix="foo", use_arrays=False, use_distributions=False)
1297
+ assert mm.matrix[0, 0] == 1.0
1298
+ assert mm.matrix[1, 1] == 0.0 # NaN skipped, stays zero
1299
+ assert mm.matrix[2, 2] == 3.0
1300
+ assert not np.isnan(mm.matrix.data).any()
1301
+
1302
+
1303
+ def test_nan_in_array_skips_insertion():
1304
+ dp = bwp.create_datapackage(sequential=True)
1305
+ dp.add_persistent_array(
1306
+ matrix="foo",
1307
+ name="a",
1308
+ indices_array=np.array([(0, 0), (1, 1)], dtype=bwp.INDICES_DTYPE),
1309
+ data_array=np.array([[1.0, np.nan], [3.0, 4.0]]),
1310
+ )
1311
+ mm = MappedMatrix(packages=[dp], matrix="foo", use_distributions=False)
1312
+ # column 0: data = [1.0, 3.0] — both inserted
1313
+ assert mm.matrix[0, 0] == 1.0
1314
+ assert mm.matrix[1, 1] == 3.0
1315
+ next(mm)
1316
+ # column 1: data = [nan, 4.0] — first element skipped, stays zero
1317
+ assert mm.matrix[0, 0] == 0.0
1318
+ assert mm.matrix[1, 1] == 4.0
1319
+ assert not np.isnan(mm.matrix.data).any()
1320
+
1321
+
1322
+ def test_nan_preserves_earlier_package_value():
1323
+ # Package A sets values; package B uses NaN to leave them untouched.
1324
+ dp_base = bwp.create_datapackage()
1325
+ dp_base.add_persistent_vector(
1326
+ matrix="foo",
1327
+ name="base",
1328
+ indices_array=np.array([(0, 0), (1, 1)], dtype=bwp.INDICES_DTYPE),
1329
+ data_array=np.array([5.0, 7.0]),
1330
+ )
1331
+ dp_scenario = bwp.create_datapackage()
1332
+ dp_scenario.add_persistent_vector(
1333
+ matrix="foo",
1334
+ name="scenario",
1335
+ indices_array=np.array([(0, 0), (1, 1)], dtype=bwp.INDICES_DTYPE),
1336
+ data_array=np.array([np.nan, 99.0]),
1337
+ )
1338
+ mm = MappedMatrix(
1339
+ packages=[dp_base, dp_scenario], matrix="foo", use_arrays=False, use_distributions=False
1340
+ )
1341
+ assert mm.matrix[0, 0] == 5.0 # NaN in scenario → base value preserved
1342
+ assert mm.matrix[1, 1] == 99.0 # non-NaN in scenario → overrides base
1343
+ assert not np.isnan(mm.matrix.data).any()
@@ -111,8 +111,8 @@ def test_distributions_without_uncertainties():
111
111
  row = mm.row_mapper.to_dict()
112
112
  col = mm.col_mapper.to_dict()
113
113
 
114
- assert 50 <= np.mean(results[row[10], col[20], :]) <= 150
115
- assert 8 <= np.mean(results[row[11], col[21], :]) <= 14
114
+ assert 20 <= np.mean(results[row[10], col[20], :]) <= 180
115
+ assert 5 <= np.mean(results[row[11], col[21], :]) <= 18
116
116
 
117
117
  assert np.allclose(results[row[10], col[10], :], 11)
118
118
  assert np.allclose(results[row[18], col[7], :], 125)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes