bw-graph-tools 0.3__tar.gz → 0.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. {bw_graph_tools-0.3 → bw_graph_tools-0.4}/LICENSE +1 -1
  2. bw_graph_tools-0.4/MANIFEST.in +0 -0
  3. {bw_graph_tools-0.3 → bw_graph_tools-0.4}/PKG-INFO +24 -30
  4. {bw_graph_tools-0.3 → bw_graph_tools-0.4}/README.md +4 -11
  5. {bw_graph_tools-0.3 → bw_graph_tools-0.4}/bw_graph_tools/__init__.py +4 -5
  6. {bw_graph_tools-0.3 → bw_graph_tools-0.4}/bw_graph_tools/graph_traversal.py +50 -38
  7. {bw_graph_tools-0.3 → bw_graph_tools-0.4}/bw_graph_tools/graph_traversal_utils.py +1 -4
  8. {bw_graph_tools-0.3 → bw_graph_tools-0.4}/bw_graph_tools/matrix_tools.py +21 -11
  9. {bw_graph_tools-0.3 → bw_graph_tools-0.4}/bw_graph_tools/shortest_path.py +40 -14
  10. {bw_graph_tools-0.3 → bw_graph_tools-0.4}/bw_graph_tools/testing.py +4 -2
  11. {bw_graph_tools-0.3 → bw_graph_tools-0.4}/bw_graph_tools.egg-info/PKG-INFO +25 -31
  12. bw_graph_tools-0.4/bw_graph_tools.egg-info/SOURCES.txt +21 -0
  13. {bw_graph_tools-0.3 → bw_graph_tools-0.4}/bw_graph_tools.egg-info/requires.txt +10 -9
  14. bw_graph_tools-0.4/bw_graph_tools.egg-info/top_level.txt +1 -0
  15. bw_graph_tools-0.4/pyproject.toml +105 -0
  16. bw_graph_tools-0.4/setup.cfg +4 -0
  17. {bw_graph_tools-0.3 → bw_graph_tools-0.4}/tests/test_first_heuristic.py +56 -4
  18. {bw_graph_tools-0.3 → bw_graph_tools-0.4}/tests/test_get_path_from_matrix.py +16 -9
  19. {bw_graph_tools-0.3 → bw_graph_tools-0.4}/tests/test_second_heuristic.py +6 -18
  20. {bw_graph_tools-0.3 → bw_graph_tools-0.4}/tests/test_third_heuristic.py +5 -15
  21. bw_graph_tools-0.3/MANIFEST.in +0 -1
  22. bw_graph_tools-0.3/bw_graph_tools/VERSION +0 -1
  23. bw_graph_tools-0.3/bw_graph_tools/utils.py +0 -15
  24. bw_graph_tools-0.3/bw_graph_tools.egg-info/SOURCES.txt +0 -31
  25. bw_graph_tools-0.3/bw_graph_tools.egg-info/not-zip-safe +0 -1
  26. bw_graph_tools-0.3/bw_graph_tools.egg-info/top_level.txt +0 -5
  27. bw_graph_tools-0.3/docs/conf.py +0 -93
  28. bw_graph_tools-0.3/pyproject.toml +0 -3
  29. bw_graph_tools-0.3/setup.cfg +0 -91
  30. bw_graph_tools-0.3/tests/traversal/test_marc_vd_meide.py +0 -160
  31. bw_graph_tools-0.3/tests/traversal/test_negative_production.py +0 -239
  32. bw_graph_tools-0.3/tests/traversal/test_nonunitary_production_with_recursion.py +0 -239
  33. bw_graph_tools-0.3/tests/traversal/test_only_one_layer.py +0 -161
  34. bw_graph_tools-0.3/tests/traversal/test_separate_production.py +0 -241
  35. {bw_graph_tools-0.3 → bw_graph_tools-0.4}/bw_graph_tools/errors.py +0 -0
  36. {bw_graph_tools-0.3 → bw_graph_tools-0.4}/bw_graph_tools.egg-info/dependency_links.txt +0 -0
  37. {bw_graph_tools-0.3 → bw_graph_tools-0.4}/tests/test_matrix_utils.py +0 -0
@@ -1,4 +1,4 @@
1
- Copyright 2023 Chris Mutel
1
+ Copyright 2024 Chris Mutel
2
2
 
3
3
  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
4
4
 
File without changes
@@ -1,43 +1,44 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: bw_graph_tools
3
- Version: 0.3
3
+ Version: 0.4
4
4
  Summary: Graph traversal class and utilities
5
- Home-page: https://github.com/brightway-lca/bw_graph_tools
6
- Author: Chris Mutel
7
- Author-email: <cmutel@gmail.com>
8
- Maintainer: Chris Mutel
9
- Maintainer-email: <cmutel@gmail.com>
10
- License: BSD-3-Clause
5
+ Author-email: Chris Mutel <cmutel@gmail.com>
6
+ Maintainer-email: Chris Mutel <cmutel@gmail.com>
11
7
  Project-URL: source, https://github.com/brightway-lca/bw_graph_tools
12
8
  Project-URL: homepage, https://github.com/brightway-lca/bw_graph_tools
13
9
  Project-URL: tracker, https://github.com/brightway-lca/bw_graph_tools/issues
14
- Keywords: "brightway","development"
15
- Platform: any
16
- Classifier: Development Status :: 4 - Beta
10
+ Classifier: Development Status :: 5 - Production/Stable
17
11
  Classifier: Programming Language :: Python
18
12
  Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.9
19
14
  Classifier: Programming Language :: Python :: 3.10
20
15
  Classifier: Programming Language :: Python :: 3.11
21
- Requires-Python: >=3.8
22
- Description-Content-Type: text/markdown; charset=UTF-8
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Natural Language :: English
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Topic :: Scientific/Engineering
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
23
22
  License-File: LICENSE
24
23
  Requires-Dist: bw2calc>=2.0.dev13
24
+ Requires-Dist: bw_processing
25
25
  Requires-Dist: matrix_utils
26
- Requires-Dist: numpy
26
+ Requires-Dist: numpy<2
27
27
  Requires-Dist: scipy
28
- Requires-Dist: bw_processing
29
28
  Provides-Extra: testing
30
- Requires-Dist: setuptools; extra == "testing"
29
+ Requires-Dist: bw_graph_tools; extra == "testing"
30
+ Requires-Dist: bw2data; extra == "testing"
31
31
  Requires-Dist: pytest; extra == "testing"
32
32
  Requires-Dist: pytest-cov; extra == "testing"
33
- Requires-Dist: bw2data>=4.0.dev18; extra == "testing"
33
+ Requires-Dist: python-coveralls; extra == "testing"
34
34
  Provides-Extra: dev
35
+ Requires-Dist: build; extra == "dev"
35
36
  Requires-Dist: pre-commit; extra == "dev"
36
37
  Requires-Dist: pylint; extra == "dev"
37
- Provides-Extra: docs
38
- Requires-Dist: sphinx; extra == "docs"
39
- Requires-Dist: myst_parser; extra == "docs"
40
- Requires-Dist: furo; extra == "docs"
38
+ Requires-Dist: pytest; extra == "dev"
39
+ Requires-Dist: pytest-cov; extra == "dev"
40
+ Requires-Dist: pytest-randomly; extra == "dev"
41
+ Requires-Dist: setuptools; extra == "dev"
41
42
 
42
43
  # bw_graph_tools
43
44
 
@@ -85,7 +86,6 @@ Normally we construct matrices and solve the resulting set of linear equations t
85
86
  If we add temporal information using `bw_temporalis`, then the same node can occur at different times depending on how the temporal dynamics its preceding path. For example:
86
87
 
87
88
 
88
-
89
89
  ## Contributing
90
90
 
91
91
  Contributions are very welcome.
@@ -103,15 +103,9 @@ please [file an issue] along with a detailed description.
103
103
 
104
104
  ## Documentation
105
105
 
106
- 1. Install the `sphinx-furo` conda environment from the file `.docs/environment.yml`.
107
- 2. Build the documentation locally by running
106
+ 1. Install the conda environment from the file `.docs/environment.yml`
107
+ 2. Build the documentation locally by running:
108
108
 
109
- ```
109
+ ```bash
110
110
  sphinx-autobuild docs _build/html -a -j auto --open-browser
111
111
  ```
112
-
113
- <!-- github-only -->
114
-
115
- [command-line reference]: https://bw_graph_tools.readthedocs.io/en/latest/usage.html
116
- [license]: https://github.com/brightway-lca/bw_graph_tools/blob/main/LICENSE
117
- [contributor guide]: https://github.com/brightway-lca/bw_graph_tools/blob/main/CONTRIBUTING.md
@@ -44,7 +44,6 @@ Normally we construct matrices and solve the resulting set of linear equations t
44
44
  If we add temporal information using `bw_temporalis`, then the same node can occur at different times depending on how the temporal dynamics its preceding path. For example:
45
45
 
46
46
 
47
-
48
47
  ## Contributing
49
48
 
50
49
  Contributions are very welcome.
@@ -62,15 +61,9 @@ please [file an issue] along with a detailed description.
62
61
 
63
62
  ## Documentation
64
63
 
65
- 1. Install the `sphinx-furo` conda environment from the file `.docs/environment.yml`.
66
- 2. Build the documentation locally by running
64
+ 1. Install the conda environment from the file `.docs/environment.yml`
65
+ 2. Build the documentation locally by running:
67
66
 
68
- ```
67
+ ```bash
69
68
  sphinx-autobuild docs _build/html -a -j auto --open-browser
70
- ```
71
-
72
- <!-- github-only -->
73
-
74
- [command-line reference]: https://bw_graph_tools.readthedocs.io/en/latest/usage.html
75
- [license]: https://github.com/brightway-lca/bw_graph_tools/blob/main/LICENSE
76
- [contributor guide]: https://github.com/brightway-lca/bw_graph_tools/blob/main/CONTRIBUTING.md
69
+ ```
@@ -11,9 +11,8 @@ __all__ = (
11
11
  "to_normalized_adjacency_matrix",
12
12
  )
13
13
 
14
- from .graph_traversal_utils import get_path_from_matrix, path_as_brightway_objects
15
- from .matrix_tools import guess_production_exchanges, to_normalized_adjacency_matrix
16
- from .utils import get_version_tuple
14
+ __version__ = "0.4"
15
+
17
16
  from .graph_traversal import (
18
17
  AssumedDiagonalGraphTraversal,
19
18
  Edge,
@@ -21,5 +20,5 @@ from .graph_traversal import (
21
20
  NewNodeEachVisitGraphTraversal,
22
21
  Node,
23
22
  )
24
-
25
- __version__ = get_version_tuple()
23
+ from .graph_traversal_utils import get_path_from_matrix, path_as_brightway_objects
24
+ from .matrix_tools import guess_production_exchanges, to_normalized_adjacency_matrix
@@ -2,11 +2,12 @@ import warnings
2
2
  from dataclasses import dataclass
3
3
  from functools import lru_cache
4
4
  from heapq import heappop, heappush
5
+ from typing import Optional
5
6
 
6
- from bw2calc import LCA
7
- from scipy.sparse import spmatrix
8
7
  import matrix_utils as mu
9
8
  import numpy as np
9
+ from bw2calc import LCA
10
+ from scipy.sparse import spmatrix
10
11
 
11
12
  try:
12
13
  from bw2data import databases
@@ -70,9 +71,15 @@ class Node:
70
71
  supply_amount : float
71
72
  The amount of the *activity* (not reference product!) needed to supply the demand from the requesting supply chain edge.
72
73
  cumulative_score : float
73
- Total LCIA score attributed to `supply_amount` of this activity. Includes direct emissions unless explicitly removed.
74
+ Total LCIA score attributed to `supply_amount` of this activity, including impacts from direct emissions.
74
75
  direct_emissions_score : float
75
76
  Total LCIA score attributed only to the direct characterized biosphere flows of `supply_amount` of this activity.
77
+ direct_emissions_score_outside_specific_flows : float
78
+ The score attributable to *direct emissions* of this node which isn't broken out into separate `Flow` objects.
79
+ remaining_cumulative_score_outside_specific_flows : float
80
+ The *cumulative* score of this node, including direct emissions, which isn't broken out into separate `Flow` objects.
81
+ terminal : bool
82
+ Boolean flag indicating whether graph traversal was cutoff at this node
76
83
 
77
84
  """
78
85
 
@@ -85,6 +92,9 @@ class Node:
85
92
  supply_amount: float
86
93
  cumulative_score: float
87
94
  direct_emissions_score: float
95
+ direct_emissions_score_outside_specific_flows: float = 0.0
96
+ remaining_cumulative_score_outside_specific_flows: float = 0.0
97
+ terminal: bool = False
88
98
 
89
99
  def __lt__(self, other):
90
100
  # Needed for sorting
@@ -176,13 +186,13 @@ class NewNodeEachVisitGraphTraversal:
176
186
  def calculate(
177
187
  cls,
178
188
  lca_object: LCA,
179
- cutoff: float | None = 5e-3,
180
- biosphere_cutoff: float | None = 1e-4,
181
- max_calc: int | None = 1e5,
182
- skip_coproducts: bool | None = False,
183
- separate_biosphere_flows: bool | None = True,
184
- static_activity_indices: set[int] | None = set(),
185
- functional_unit_unique_id: int | None = -1,
189
+ cutoff: Optional[float] = 5e-3,
190
+ biosphere_cutoff: Optional[float] = 1e-4,
191
+ max_calc: Optional[int] = 10000,
192
+ skip_coproducts: Optional[bool] = False,
193
+ separate_biosphere_flows: Optional[bool] = True,
194
+ static_activity_indices: Optional[set[int]] = set(),
195
+ functional_unit_unique_id: Optional[int] = -1,
186
196
  ) -> dict:
187
197
  """
188
198
  Priority-first traversal (i.e. follow the past of highest score) of the supply chain graph. This class unrolls
@@ -240,8 +250,8 @@ class NewNodeEachVisitGraphTraversal:
240
250
  traverse
241
251
  functional_unit_unique_id : int
242
252
  An integer id we can use for the functional unit virtual activity.
243
- Shouldn't overlap any other activity ids. Don't change unless you r
244
- eally know what you are doing.
253
+ Shouldn't overlap any other activity ids. Don't change unless you
254
+ really know what you are doing.
245
255
 
246
256
  Returns
247
257
  -------
@@ -257,8 +267,7 @@ class NewNodeEachVisitGraphTraversal:
257
267
  biosphere_cutoff_score = abs(total_score * biosphere_cutoff)
258
268
  technosphere_matrix = lca_object.technosphere_matrix
259
269
  production_exchange_mapping = {
260
- x: y
261
- for x, y in zip(*cls.get_production_exchanges(lca_object.technosphere_mm))
270
+ x: y for x, y in zip(*cls.get_production_exchanges(lca_object.technosphere_mm))
262
271
  }
263
272
  heap, edges, flows = [], [], []
264
273
  calculation_count = Counter()
@@ -282,9 +291,7 @@ class NewNodeEachVisitGraphTraversal:
282
291
  cls.traverse_edges(
283
292
  consumer_index=functional_unit_unique_id,
284
293
  consumer_unique_id=functional_unit_unique_id,
285
- product_indices=[
286
- lca_object.dicts.product[key] for key in lca_object.demand
287
- ],
294
+ product_indices=[lca_object.dicts.product[key] for key in lca_object.demand],
288
295
  product_amounts=lca_object.demand.values(),
289
296
  lca=lca_object,
290
297
  calculation_count=calculation_count,
@@ -323,6 +330,11 @@ class NewNodeEachVisitGraphTraversal:
323
330
 
324
331
  flows.sort(reverse=True)
325
332
 
333
+ non_terminal_nodes = {edge.consumer_unique_id for edge in edges}
334
+ for node_id_key in nodes:
335
+ if node_id_key not in non_terminal_nodes:
336
+ nodes[node_id_key].terminal = True
337
+
326
338
  return {
327
339
  "nodes": nodes,
328
340
  "edges": edges,
@@ -461,9 +473,7 @@ class NewNodeEachVisitGraphTraversal:
461
473
 
462
474
  supply = caching_solver(product_index, product_amount)
463
475
  cumulative_score = float((characterized_biosphere * supply).sum())
464
- reference_product_net_production_amount = matrix[
465
- product_index, producer_index
466
- ]
476
+ reference_product_net_production_amount = matrix[product_index, producer_index]
467
477
  scale = product_amount / reference_product_net_production_amount
468
478
 
469
479
  if abs(cumulative_score) < cutoff_score:
@@ -473,16 +483,12 @@ class NewNodeEachVisitGraphTraversal:
473
483
  unique_id=next(calculation_count),
474
484
  activity_datapackage_id=lca.dicts.activity.reversed[producer_index],
475
485
  activity_index=producer_index,
476
- reference_product_datapackage_id=lca.dicts.product.reversed[
477
- product_index
478
- ],
486
+ reference_product_datapackage_id=lca.dicts.product.reversed[product_index],
479
487
  reference_product_index=product_index,
480
488
  reference_product_production_amount=reference_product_net_production_amount,
481
489
  supply_amount=scale,
482
490
  cumulative_score=cumulative_score,
483
- direct_emissions_score=(
484
- scale * characterized_biosphere[:, producer_index]
485
- ).sum(),
491
+ direct_emissions_score=(scale * characterized_biosphere[:, producer_index]).sum(),
486
492
  )
487
493
  edges.append(
488
494
  Edge(
@@ -496,13 +502,22 @@ class NewNodeEachVisitGraphTraversal:
496
502
  )
497
503
 
498
504
  if separate_biosphere_flows:
499
- cls.add_biosphere_flows(
505
+ flow_score = cls.add_biosphere_flows(
500
506
  flows=flows,
501
507
  matrix=(scale * characterized_biosphere[:, producer_index]).tocoo(),
502
508
  lca=lca,
503
509
  node=producing_node,
504
510
  biosphere_cutoff_score=biosphere_cutoff_score,
505
511
  )
512
+ else:
513
+ flow_score = 0
514
+
515
+ producing_node.direct_emissions_score_outside_specific_flows = (
516
+ producing_node.direct_emissions_score - flow_score
517
+ )
518
+ producing_node.remaining_cumulative_score_outside_specific_flows = (
519
+ producing_node.cumulative_score - flow_score
520
+ )
506
521
 
507
522
  heappush(heap, (abs(1 / cumulative_score), producing_node))
508
523
  nodes[producing_node.unique_id] = producing_node
@@ -529,9 +544,7 @@ class NewNodeEachVisitGraphTraversal:
529
544
  return lca.characterization_matrix * lca.biosphere_matrix
530
545
 
531
546
  @classmethod
532
- def get_production_exchanges(
533
- cls, mapped_matrix: mu.MappedMatrix
534
- ) -> (np.array, np.array):
547
+ def get_production_exchanges(cls, mapped_matrix: mu.MappedMatrix) -> (np.array, np.array):
535
548
  """
536
549
  Get matrix row and column indices of productions exchanges by trying a
537
550
  series of heuristics. See documentation for
@@ -563,7 +576,7 @@ class NewNodeEachVisitGraphTraversal:
563
576
  lca: LCA,
564
577
  node: Node,
565
578
  biosphere_cutoff_score: float,
566
- ) -> None:
579
+ ) -> float:
567
580
  """
568
581
  Add individual biosphere flows as `Flow` instances to `flow` if their score is above `biosphere_cutoff_score`.
569
582
 
@@ -582,12 +595,13 @@ class NewNodeEachVisitGraphTraversal:
582
595
 
583
596
  Returns
584
597
  -------
585
- `None`
586
- Modifies `flows` by adding new `Flow` instances
598
+ The total LCIA score broken out to separate `Flow` instances
587
599
 
588
600
  """
601
+ added_score = 0.0
589
602
  for index, score in zip(matrix.row, matrix.data):
590
603
  if abs(score) > biosphere_cutoff_score:
604
+ added_score += score
591
605
  flows.append(
592
606
  Flow(
593
607
  flow_datapackage_id=lca.dicts.biosphere.reversed[index],
@@ -596,12 +610,12 @@ class NewNodeEachVisitGraphTraversal:
596
610
  activity_id=node.activity_datapackage_id,
597
611
  activity_index=node.activity_index,
598
612
  amount=(
599
- lca.biosphere_matrix[index, node.activity_index]
600
- * node.supply_amount
613
+ lca.biosphere_matrix[index, node.activity_index] * node.supply_amount
601
614
  ),
602
615
  score=score,
603
616
  )
604
617
  )
618
+ return added_score
605
619
 
606
620
  @classmethod
607
621
  def get_demand_vector_for_activity(
@@ -648,9 +662,7 @@ class NewNodeEachVisitGraphTraversal:
648
662
 
649
663
  class AssumedDiagonalGraphTraversal(NewNodeEachVisitGraphTraversal):
650
664
  @classmethod
651
- def get_production_exchanges(
652
- cls, mapped_matrix: mu.MappedMatrix
653
- ) -> (np.ndarray, np.ndarray):
665
+ def get_production_exchanges(cls, mapped_matrix: mu.MappedMatrix) -> (np.ndarray, np.ndarray):
654
666
  """
655
667
  Assume production exchanges are always on the diagonal instead of
656
668
  examining matrix structure and input data.
@@ -60,10 +60,7 @@ def path_as_brightway_objects(
60
60
  (
61
61
  bd.get_node(id=lca.dicts.product.reversed[x]),
62
62
  bd.get_node(id=lca.dicts.activity.reversed[y]),
63
- -1
64
- * lca.technosphere_matrix[
65
- y, x
66
- ], # Flip x and y because y is input to activity x
63
+ -1 * lca.technosphere_matrix[y, x], # Flip x and y because y is input to activity x
67
64
  )
68
65
  for x, y in zip(path[:-1], path[1:])
69
66
  ]
@@ -67,8 +67,24 @@ def gpe_first_heuristic(mm: mu.MappedMatrix) -> Tuple[np.ndarray, np.ndarray]:
67
67
  # Need to check the original input values, not after mapping when they are
68
68
  # normalized to [0, X] range.
69
69
  ident_mask = indices["row"] == indices["col"]
70
- row_mapped = group.row_mapper.map_array(indices["row"][ident_mask])
71
- col_mapped = group.col_mapper.map_array(indices["col"][ident_mask])
70
+
71
+ # In theory these values should be on the diagonal, as the input row and col
72
+ # values are the same. However, we don't have a strong guarantee that this is
73
+ # true. So to eliminate duplicate values we combine to a (X, 2) array, and get
74
+ # unique rows.
75
+ # Note that `unique` also does sorting, see
76
+ # https://stackoverflow.com/questions/16970982/find-unique-rows-in-numpy-array
77
+ combined = np.unique(
78
+ np.vstack(
79
+ (
80
+ group.row_mapper.map_array(indices["row"][ident_mask]),
81
+ group.col_mapper.map_array(indices["col"][ident_mask]),
82
+ )
83
+ ),
84
+ axis=1,
85
+ )
86
+ row_mapped = combined[0, :]
87
+ col_mapped = combined[1, :]
72
88
 
73
89
  if (row_mapped == -1).sum() or (col_mapped == -1).sum():
74
90
  ERROR = """
@@ -116,9 +132,7 @@ def gpe_second_heuristic(
116
132
  existing_mask = np.in1d(h2_col_indices, col_existing)
117
133
 
118
134
  # Need mask with correct shape for both row and col indices
119
- mask = np.in1d(
120
- group.col_masked[~group.flip], h2_col_indices[~existing_mask]
121
- )
135
+ mask = np.in1d(group.col_masked[~group.flip], h2_col_indices[~existing_mask])
122
136
 
123
137
  not_flipped.append(
124
138
  (
@@ -184,9 +198,7 @@ def guess_production_exchanges(mm: mu.MappedMatrix) -> Tuple[np.ndarray, np.ndar
184
198
  # Every column must have an activity with some reference product or the system
185
199
  # is not solvable. Therefore we can look across all columns. We will do
186
200
  # all the work in matrix indices.
187
- missing = np.setdiff1d(
188
- np.arange(mm.matrix.shape[0]), col_indices, assume_unique=True
189
- )
201
+ missing = np.setdiff1d(np.arange(mm.matrix.shape[0]), col_indices, assume_unique=True)
190
202
 
191
203
  # Short circuit other steps if possible; assumption is that this step will
192
204
  # be taken for most matrices
@@ -200,9 +212,7 @@ def guess_production_exchanges(mm: mu.MappedMatrix) -> Tuple[np.ndarray, np.ndar
200
212
  if row_indices.shape != col_indices.shape:
201
213
  raise ValueError("Guessed row indices do not match guessed column indices.")
202
214
 
203
- missing = np.setdiff1d(
204
- np.arange(mm.matrix.shape[0]), col_indices, assume_unique=True
205
- )
215
+ missing = np.setdiff1d(np.arange(mm.matrix.shape[0]), col_indices, assume_unique=True)
206
216
  if missing.size:
207
217
  raise UnclearProductionExchange(
208
218
  "Can't find production exchanges for columns: {}".format(list(missing))
@@ -36,16 +36,23 @@ OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISE
36
36
  OF THE POSSIBILITY OF SUCH DAMAGE.
37
37
 
38
38
  """
39
+
39
40
  from functools import partial
40
41
  from multiprocessing import Pool
41
- from typing import Optional, Union, Iterable
42
+ from typing import Iterable, Optional, Union
42
43
 
43
44
  import numpy as np
44
45
  from scipy import sparse
45
46
 
46
47
 
47
- def get_distances(adjacency: sparse.csr_matrix, sources: Optional[Union[int, Iterable]] = None, method: str = 'D',
48
- return_predecessors: bool = False, unweighted: bool = False, n_jobs: Optional[int] = None):
48
+ def get_distances(
49
+ adjacency: sparse.csr_matrix,
50
+ sources: Optional[Union[int, Iterable]] = None,
51
+ method: str = "D",
52
+ return_predecessors: bool = False,
53
+ unweighted: bool = False,
54
+ n_jobs: Optional[int] = None,
55
+ ):
49
56
  """Compute distances between nodes.
50
57
 
51
58
  * Graphs
@@ -88,28 +95,40 @@ def get_distances(adjacency: sparse.csr_matrix, sources: Optional[Union[int, Ite
88
95
 
89
96
  """
90
97
  n_jobs, directed = 1, True
91
- if method == 'FW' and n_jobs != 1:
92
- raise ValueError('The Floyd-Warshall algorithm cannot be used with parallel computations.')
98
+ if method == "FW" and n_jobs != 1:
99
+ raise ValueError("The Floyd-Warshall algorithm cannot be used with parallel computations.")
93
100
  if sources is None:
94
101
  sources = np.arange(adjacency.shape[0])
95
102
  elif np.issubdtype(type(sources), np.integer):
96
103
  sources = np.array([sources])
97
104
  n = len(sources)
98
- local_function = partial(sparse.csgraph.shortest_path,
99
- adjacency, method, directed, return_predecessors, unweighted, False)
105
+ local_function = partial(
106
+ sparse.csgraph.shortest_path,
107
+ adjacency,
108
+ method,
109
+ directed,
110
+ return_predecessors,
111
+ unweighted,
112
+ False,
113
+ )
100
114
  if n_jobs == 1 or n == 1:
101
115
  try:
102
- res = sparse.csgraph.shortest_path(adjacency, method, directed, return_predecessors,
103
- unweighted, False, sources)
116
+ res = sparse.csgraph.shortest_path(
117
+ adjacency, method, directed, return_predecessors, unweighted, False, sources
118
+ )
104
119
  except sparse.csgraph.NegativeCycleError:
105
- raise ValueError("The shortest path computation could not be completed because a negative cycle is present.")
120
+ raise ValueError(
121
+ "The shortest path computation could not be completed because a negative cycle is present."
122
+ )
106
123
  else:
107
124
  try:
108
125
  with Pool(n_jobs) as pool:
109
126
  res = np.array(pool.map(local_function, sources))
110
127
  except sparse.csgraph.NegativeCycleError:
111
128
  pool.terminate()
112
- raise ValueError("The shortest path computation could not be completed because a negative cycle is present.")
129
+ raise ValueError(
130
+ "The shortest path computation could not be completed because a negative cycle is present."
131
+ )
113
132
  if return_predecessors:
114
133
  res[1][res[1] < 0] = -1
115
134
  if n == 1:
@@ -123,8 +142,14 @@ def get_distances(adjacency: sparse.csr_matrix, sources: Optional[Union[int, Ite
123
142
  return res
124
143
 
125
144
 
126
- def get_shortest_path(adjacency: sparse.csr_matrix, sources: Union[int, Iterable], targets: Union[int, Iterable],
127
- method: str = 'D', unweighted: bool = False, n_jobs: Optional[int] = None):
145
+ def get_shortest_path(
146
+ adjacency: sparse.csr_matrix,
147
+ sources: Union[int, Iterable],
148
+ targets: Union[int, Iterable],
149
+ method: str = "D",
150
+ unweighted: bool = False,
151
+ n_jobs: Optional[int] = None,
152
+ ):
128
153
  """Compute the shortest paths in the graph.
129
154
 
130
155
  Parameters
@@ -181,7 +206,8 @@ def get_shortest_path(adjacency: sparse.csr_matrix, sources: Union[int, Iterable
181
206
  targets = sources
182
207
  else:
183
208
  raise ValueError(
184
- 'This request is ambiguous. Either use one source and multiple targets or multiple sources and one target.')
209
+ "This request is ambiguous. Either use one source and multiple targets or multiple sources and one target."
210
+ )
185
211
 
186
212
  if source2target:
187
213
  dists, preds = get_distances(adjacency, source, method, True, unweighted, n_jobs)
@@ -1,10 +1,12 @@
1
1
  from numbers import Number
2
+ from typing import Union
3
+
2
4
  import numpy as np
3
5
 
4
- from . import Node, Edge, Flow
6
+ from . import Edge, Flow, Node
5
7
 
6
8
 
7
- def equal_dict(a: Node | Edge | Flow, b: dict, fields: list[str]):
9
+ def equal_dict(a: Union[Node, Edge, Flow], b: dict, fields: list[str]):
8
10
  for field in fields:
9
11
  if field in b:
10
12
  if isinstance(b[field], Number):
@@ -1,43 +1,44 @@
1
1
  Metadata-Version: 2.1
2
- Name: bw-graph-tools
3
- Version: 0.3
2
+ Name: bw_graph_tools
3
+ Version: 0.4
4
4
  Summary: Graph traversal class and utilities
5
- Home-page: https://github.com/brightway-lca/bw_graph_tools
6
- Author: Chris Mutel
7
- Author-email: <cmutel@gmail.com>
8
- Maintainer: Chris Mutel
9
- Maintainer-email: <cmutel@gmail.com>
10
- License: BSD-3-Clause
5
+ Author-email: Chris Mutel <cmutel@gmail.com>
6
+ Maintainer-email: Chris Mutel <cmutel@gmail.com>
11
7
  Project-URL: source, https://github.com/brightway-lca/bw_graph_tools
12
8
  Project-URL: homepage, https://github.com/brightway-lca/bw_graph_tools
13
9
  Project-URL: tracker, https://github.com/brightway-lca/bw_graph_tools/issues
14
- Keywords: "brightway","development"
15
- Platform: any
16
- Classifier: Development Status :: 4 - Beta
10
+ Classifier: Development Status :: 5 - Production/Stable
17
11
  Classifier: Programming Language :: Python
18
12
  Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.9
19
14
  Classifier: Programming Language :: Python :: 3.10
20
15
  Classifier: Programming Language :: Python :: 3.11
21
- Requires-Python: >=3.8
22
- Description-Content-Type: text/markdown; charset=UTF-8
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Natural Language :: English
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Topic :: Scientific/Engineering
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
23
22
  License-File: LICENSE
24
23
  Requires-Dist: bw2calc>=2.0.dev13
24
+ Requires-Dist: bw_processing
25
25
  Requires-Dist: matrix_utils
26
- Requires-Dist: numpy
26
+ Requires-Dist: numpy<2
27
27
  Requires-Dist: scipy
28
- Requires-Dist: bw_processing
29
28
  Provides-Extra: testing
30
- Requires-Dist: setuptools; extra == "testing"
29
+ Requires-Dist: bw_graph_tools; extra == "testing"
30
+ Requires-Dist: bw2data; extra == "testing"
31
31
  Requires-Dist: pytest; extra == "testing"
32
32
  Requires-Dist: pytest-cov; extra == "testing"
33
- Requires-Dist: bw2data>=4.0.dev18; extra == "testing"
33
+ Requires-Dist: python-coveralls; extra == "testing"
34
34
  Provides-Extra: dev
35
+ Requires-Dist: build; extra == "dev"
35
36
  Requires-Dist: pre-commit; extra == "dev"
36
37
  Requires-Dist: pylint; extra == "dev"
37
- Provides-Extra: docs
38
- Requires-Dist: sphinx; extra == "docs"
39
- Requires-Dist: myst_parser; extra == "docs"
40
- Requires-Dist: furo; extra == "docs"
38
+ Requires-Dist: pytest; extra == "dev"
39
+ Requires-Dist: pytest-cov; extra == "dev"
40
+ Requires-Dist: pytest-randomly; extra == "dev"
41
+ Requires-Dist: setuptools; extra == "dev"
41
42
 
42
43
  # bw_graph_tools
43
44
 
@@ -85,7 +86,6 @@ Normally we construct matrices and solve the resulting set of linear equations t
85
86
  If we add temporal information using `bw_temporalis`, then the same node can occur at different times depending on how the temporal dynamics its preceding path. For example:
86
87
 
87
88
 
88
-
89
89
  ## Contributing
90
90
 
91
91
  Contributions are very welcome.
@@ -103,15 +103,9 @@ please [file an issue] along with a detailed description.
103
103
 
104
104
  ## Documentation
105
105
 
106
- 1. Install the `sphinx-furo` conda environment from the file `.docs/environment.yml`.
107
- 2. Build the documentation locally by running
106
+ 1. Install the conda environment from the file `.docs/environment.yml`
107
+ 2. Build the documentation locally by running:
108
108
 
109
- ```
109
+ ```bash
110
110
  sphinx-autobuild docs _build/html -a -j auto --open-browser
111
111
  ```
112
-
113
- <!-- github-only -->
114
-
115
- [command-line reference]: https://bw_graph_tools.readthedocs.io/en/latest/usage.html
116
- [license]: https://github.com/brightway-lca/bw_graph_tools/blob/main/LICENSE
117
- [contributor guide]: https://github.com/brightway-lca/bw_graph_tools/blob/main/CONTRIBUTING.md