cooptools 1.61__tar.gz → 1.63__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 (167) hide show
  1. {cooptools-1.61 → cooptools-1.63}/PKG-INFO +1 -1
  2. {cooptools-1.61 → cooptools-1.63}/cooptools/graphs/graph.py +339 -67
  3. {cooptools-1.61 → cooptools-1.63}/cooptools.egg-info/PKG-INFO +1 -1
  4. {cooptools-1.61 → cooptools-1.63}/setup.py +1 -1
  5. {cooptools-1.61 → cooptools-1.63}/MANIFEST.in +0 -0
  6. {cooptools-1.61 → cooptools-1.63}/README.md +0 -0
  7. {cooptools-1.61 → cooptools-1.63}/cooptools/__init__.py +0 -0
  8. {cooptools-1.61 → cooptools-1.63}/cooptools/anchor.py +0 -0
  9. {cooptools-1.61 → cooptools-1.63}/cooptools/asyncable.py +0 -0
  10. {cooptools-1.61 → cooptools-1.63}/cooptools/cli/CliAtomicUserInteraction.py +0 -0
  11. {cooptools-1.61 → cooptools-1.63}/cooptools/cli/CliMenu.py +0 -0
  12. {cooptools-1.61 → cooptools-1.63}/cooptools/cli/__init__.py +0 -0
  13. {cooptools-1.61 → cooptools-1.63}/cooptools/cli/fileContentReturn.py +0 -0
  14. {cooptools-1.61 → cooptools-1.63}/cooptools/cnxn_info.py +0 -0
  15. {cooptools-1.61 → cooptools-1.63}/cooptools/colors.py +0 -0
  16. {cooptools-1.61 → cooptools-1.63}/cooptools/commandDesignPattern/__init__.py +0 -0
  17. {cooptools-1.61 → cooptools-1.63}/cooptools/commandDesignPattern/commandController.py +0 -0
  18. {cooptools-1.61 → cooptools-1.63}/cooptools/commandDesignPattern/commandProtocol.py +0 -0
  19. {cooptools-1.61 → cooptools-1.63}/cooptools/commandDesignPattern/exceptions.py +0 -0
  20. {cooptools-1.61 → cooptools-1.63}/cooptools/common.py +0 -0
  21. {cooptools-1.61 → cooptools-1.63}/cooptools/config.py +0 -0
  22. {cooptools-1.61 → cooptools-1.63}/cooptools/coopDataclass.py +0 -0
  23. {cooptools-1.61 → cooptools-1.63}/cooptools/coopEnum.py +0 -0
  24. {cooptools-1.61 → cooptools-1.63}/cooptools/coopthreading.py +0 -0
  25. {cooptools-1.61 → cooptools-1.63}/cooptools/currency.py +0 -0
  26. {cooptools-1.61 → cooptools-1.63}/cooptools/dataRefresher/__init__.py +0 -0
  27. {cooptools-1.61 → cooptools-1.63}/cooptools/dataRefresher/dataHub.py +0 -0
  28. {cooptools-1.61 → cooptools-1.63}/cooptools/dataRefresher/dataRefresher.py +0 -0
  29. {cooptools-1.61 → cooptools-1.63}/cooptools/dataStore/__init__.py +0 -0
  30. {cooptools-1.61 → cooptools-1.63}/cooptools/dataStore/dataProcessor.py +0 -0
  31. {cooptools-1.61 → cooptools-1.63}/cooptools/dataStore/dataStoreProtocol.py +0 -0
  32. {cooptools-1.61 → cooptools-1.63}/cooptools/dataStore/dbConnectionURI.py +0 -0
  33. {cooptools-1.61 → cooptools-1.63}/cooptools/dataStore/inMemoryDataStore.py +0 -0
  34. {cooptools-1.61 → cooptools-1.63}/cooptools/date_utils.py +0 -0
  35. {cooptools-1.61 → cooptools-1.63}/cooptools/decay.py +0 -0
  36. {cooptools-1.61 → cooptools-1.63}/cooptools/decor.py +0 -0
  37. {cooptools-1.61 → cooptools-1.63}/cooptools/dictPolicies.py +0 -0
  38. {cooptools-1.61 → cooptools-1.63}/cooptools/exceptions.py +0 -0
  39. {cooptools-1.61 → cooptools-1.63}/cooptools/expertise/__init__.py +0 -0
  40. {cooptools-1.61 → cooptools-1.63}/cooptools/expertise/expertiseArgs.py +0 -0
  41. {cooptools-1.61 → cooptools-1.63}/cooptools/expertise/expertiseSchedules.py +0 -0
  42. {cooptools-1.61 → cooptools-1.63}/cooptools/finance/__init__.py +0 -0
  43. {cooptools-1.61 → cooptools-1.63}/cooptools/finance/dicounted_cashflow_analysis.py +0 -0
  44. {cooptools-1.61 → cooptools-1.63}/cooptools/finance/fund_projection.py +0 -0
  45. {cooptools-1.61 → cooptools-1.63}/cooptools/finance/fund_projection_renderer.py +0 -0
  46. {cooptools-1.61 → cooptools-1.63}/cooptools/finance/futureValueProjector.py +0 -0
  47. {cooptools-1.61 → cooptools-1.63}/cooptools/finance/growth_projections.py +0 -0
  48. {cooptools-1.61 → cooptools-1.63}/cooptools/finance/reports.py +0 -0
  49. {cooptools-1.61 → cooptools-1.63}/cooptools/finance/utils.py +0 -0
  50. {cooptools-1.61 → cooptools-1.63}/cooptools/geometry_utils/__init__.py +0 -0
  51. {cooptools-1.61 → cooptools-1.63}/cooptools/geometry_utils/circle_utils.py +0 -0
  52. {cooptools-1.61 → cooptools-1.63}/cooptools/geometry_utils/common.py +0 -0
  53. {cooptools-1.61 → cooptools-1.63}/cooptools/geometry_utils/curve_utils.py +0 -0
  54. {cooptools-1.61 → cooptools-1.63}/cooptools/geometry_utils/line_utils.py +0 -0
  55. {cooptools-1.61 → cooptools-1.63}/cooptools/geometry_utils/polygon_utils.py +0 -0
  56. {cooptools-1.61 → cooptools-1.63}/cooptools/geometry_utils/rect_utils.py +0 -0
  57. {cooptools-1.61 → cooptools-1.63}/cooptools/geometry_utils/triangle_utils.py +0 -0
  58. {cooptools-1.61 → cooptools-1.63}/cooptools/geometry_utils/vector_utils.py +0 -0
  59. {cooptools-1.61 → cooptools-1.63}/cooptools/graphs/__init__.py +0 -0
  60. {cooptools-1.61 → cooptools-1.63}/cooptools/graphs/astar_results_viewer.py +0 -0
  61. {cooptools-1.61 → cooptools-1.63}/cooptools/graphs/draw.py +0 -0
  62. {cooptools-1.61 → cooptools-1.63}/cooptools/graphs/graph_dcs.py +0 -0
  63. {cooptools-1.61 → cooptools-1.63}/cooptools/graphs/graph_definitions.py +0 -0
  64. {cooptools-1.61 → cooptools-1.63}/cooptools/graphs/utils.py +0 -0
  65. {cooptools-1.61 → cooptools-1.63}/cooptools/ideas/__init__.py +0 -0
  66. {cooptools-1.61 → cooptools-1.63}/cooptools/ideas/actionItemTracker/__init__.py +0 -0
  67. {cooptools-1.61 → cooptools-1.63}/cooptools/ideas/actionItemTracker/ai_tracker.py +0 -0
  68. {cooptools-1.61 → cooptools-1.63}/cooptools/ideas/actionItemTracker/dcs.py +0 -0
  69. {cooptools-1.61 → cooptools-1.63}/cooptools/ideas/discreteEventSimulator/__init__.py +0 -0
  70. {cooptools-1.61 → cooptools-1.63}/cooptools/ideas/discreteEventSimulator/discreteEventSimulator.py +0 -0
  71. {cooptools-1.61 → cooptools-1.63}/cooptools/ideas/jsonIO.py +0 -0
  72. {cooptools-1.61 → cooptools-1.63}/cooptools/ideas/metrics.py +0 -0
  73. {cooptools-1.61 → cooptools-1.63}/cooptools/ideas/triggerActionSystem/__init__.py +0 -0
  74. {cooptools-1.61 → cooptools-1.63}/cooptools/ideas/triggerActionSystem/signal.py +0 -0
  75. {cooptools-1.61 → cooptools-1.63}/cooptools/ideas/triggerActionSystem/trigger.py +0 -0
  76. {cooptools-1.61 → cooptools-1.63}/cooptools/loggingHelpers.py +0 -0
  77. {cooptools-1.61 → cooptools-1.63}/cooptools/marchingSquares.py +0 -0
  78. {cooptools-1.61 → cooptools-1.63}/cooptools/materialHandling/__init__.py +0 -0
  79. {cooptools-1.61 → cooptools-1.63}/cooptools/materialHandling/cli.py +0 -0
  80. {cooptools-1.61 → cooptools-1.63}/cooptools/materialHandling/dcs.py +0 -0
  81. {cooptools-1.61 → cooptools-1.63}/cooptools/materialHandling/selectionCriteria.py +0 -0
  82. {cooptools-1.61 → cooptools-1.63}/cooptools/matrixManipulation.py +0 -0
  83. {cooptools-1.61 → cooptools-1.63}/cooptools/os_manip.py +0 -0
  84. {cooptools-1.61 → cooptools-1.63}/cooptools/pandasHelpers.py +0 -0
  85. {cooptools-1.61 → cooptools-1.63}/cooptools/physics/__init__.py +0 -0
  86. {cooptools-1.61 → cooptools-1.63}/cooptools/physics/kinematic.py +0 -0
  87. {cooptools-1.61 → cooptools-1.63}/cooptools/physics/kinematic_utils.py +0 -0
  88. {cooptools-1.61 → cooptools-1.63}/cooptools/physics/movement.py +0 -0
  89. {cooptools-1.61 → cooptools-1.63}/cooptools/physics/trajectory.py +0 -0
  90. {cooptools-1.61 → cooptools-1.63}/cooptools/physics/waypoint.py +0 -0
  91. {cooptools-1.61 → cooptools-1.63}/cooptools/physics/waypointFollower.py +0 -0
  92. {cooptools-1.61 → cooptools-1.63}/cooptools/plotting.py +0 -0
  93. {cooptools-1.61 → cooptools-1.63}/cooptools/printing.py +0 -0
  94. {cooptools-1.61 → cooptools-1.63}/cooptools/protocols.py +0 -0
  95. {cooptools-1.61 → cooptools-1.63}/cooptools/qualifiers/__init__.py +0 -0
  96. {cooptools-1.61 → cooptools-1.63}/cooptools/qualifiers/cli.py +0 -0
  97. {cooptools-1.61 → cooptools-1.63}/cooptools/qualifiers/qualifier.py +0 -0
  98. {cooptools-1.61 → cooptools-1.63}/cooptools/randoms.py +0 -0
  99. {cooptools-1.61 → cooptools-1.63}/cooptools/register.py +0 -0
  100. {cooptools-1.61 → cooptools-1.63}/cooptools/reservation/__init__.py +0 -0
  101. {cooptools-1.61 → cooptools-1.63}/cooptools/reservation/dcs.py +0 -0
  102. {cooptools-1.61 → cooptools-1.63}/cooptools/reservation/enums.py +0 -0
  103. {cooptools-1.61 → cooptools-1.63}/cooptools/reservation/reservationmanager.py +0 -0
  104. {cooptools-1.61 → cooptools-1.63}/cooptools/retry.py +0 -0
  105. {cooptools-1.61 → cooptools-1.63}/cooptools/sectors/__init__.py +0 -0
  106. {cooptools-1.61 → cooptools-1.63}/cooptools/sectors/grids/__init__.py +0 -0
  107. {cooptools-1.61 → cooptools-1.63}/cooptools/sectors/grids/gridState.py +0 -0
  108. {cooptools-1.61 → cooptools-1.63}/cooptools/sectors/grids/grid_base.py +0 -0
  109. {cooptools-1.61 → cooptools-1.63}/cooptools/sectors/grids/hexGrid.py +0 -0
  110. {cooptools-1.61 → cooptools-1.63}/cooptools/sectors/grids/rectGrid.py +0 -0
  111. {cooptools-1.61 → cooptools-1.63}/cooptools/sectors/hex_utils.py +0 -0
  112. {cooptools-1.61 → cooptools-1.63}/cooptools/sectors/sect_utils.py +0 -0
  113. {cooptools-1.61 → cooptools-1.63}/cooptools/sectors/sectorTree/__init__.py +0 -0
  114. {cooptools-1.61 → cooptools-1.63}/cooptools/sectors/sectorTree/sectorTree.py +0 -0
  115. {cooptools-1.61 → cooptools-1.63}/cooptools/statistics/__init__.py +0 -0
  116. {cooptools-1.61 → cooptools-1.63}/cooptools/statistics/activityReport.py +0 -0
  117. {cooptools-1.61 → cooptools-1.63}/cooptools/statistics/controlChart/__init__.py +0 -0
  118. {cooptools-1.61 → cooptools-1.63}/cooptools/statistics/controlChart/controlChart.py +0 -0
  119. {cooptools-1.61 → cooptools-1.63}/cooptools/statistics/controlChart/plotting.py +0 -0
  120. {cooptools-1.61 → cooptools-1.63}/cooptools/statistics/oee/__init__.py +0 -0
  121. {cooptools-1.61 → cooptools-1.63}/cooptools/statistics/oee/oeeHub.py +0 -0
  122. {cooptools-1.61 → cooptools-1.63}/cooptools/taskProcessing/__init__.py +0 -0
  123. {cooptools-1.61 → cooptools-1.63}/cooptools/taskProcessing/dcs.py +0 -0
  124. {cooptools-1.61 → cooptools-1.63}/cooptools/taskProcessing/taskProcessor.py +0 -0
  125. {cooptools-1.61 → cooptools-1.63}/cooptools/timeTracker/__init__.py +0 -0
  126. {cooptools-1.61 → cooptools-1.63}/cooptools/timeTracker/decay.py +0 -0
  127. {cooptools-1.61 → cooptools-1.63}/cooptools/timeTracker/timeTracker.py +0 -0
  128. {cooptools-1.61 → cooptools-1.63}/cooptools/timeWindow.py +0 -0
  129. {cooptools-1.61 → cooptools-1.63}/cooptools/toggles.py +0 -0
  130. {cooptools-1.61 → cooptools-1.63}/cooptools/transform.py +0 -0
  131. {cooptools-1.61 → cooptools-1.63}/cooptools/trends.py +0 -0
  132. {cooptools-1.61 → cooptools-1.63}/cooptools/typeProviders.py +0 -0
  133. {cooptools-1.61 → cooptools-1.63}/cooptools/typevalidation.py +0 -0
  134. {cooptools-1.61 → cooptools-1.63}/cooptools/version.py +0 -0
  135. {cooptools-1.61 → cooptools-1.63}/cooptools.egg-info/SOURCES.txt +0 -0
  136. {cooptools-1.61 → cooptools-1.63}/cooptools.egg-info/dependency_links.txt +0 -0
  137. {cooptools-1.61 → cooptools-1.63}/cooptools.egg-info/not-zip-safe +0 -0
  138. {cooptools-1.61 → cooptools-1.63}/cooptools.egg-info/requires.txt +0 -0
  139. {cooptools-1.61 → cooptools-1.63}/cooptools.egg-info/top_level.txt +0 -0
  140. {cooptools-1.61 → cooptools-1.63}/requirements.txt +0 -0
  141. {cooptools-1.61 → cooptools-1.63}/setup.cfg +0 -0
  142. {cooptools-1.61 → cooptools-1.63}/tests/test_CliAtomicUserInteraction.py +0 -0
  143. {cooptools-1.61 → cooptools-1.63}/tests/test_asyncable.py +0 -0
  144. {cooptools-1.61 → cooptools-1.63}/tests/test_cdp_bugs.py +0 -0
  145. {cooptools-1.61 → cooptools-1.63}/tests/test_commandDesignPattern.py +0 -0
  146. {cooptools-1.61 → cooptools-1.63}/tests/test_common.py +0 -0
  147. {cooptools-1.61 → cooptools-1.63}/tests/test_config.py +0 -0
  148. {cooptools-1.61 → cooptools-1.63}/tests/test_coopEnum.py +0 -0
  149. {cooptools-1.61 → cooptools-1.63}/tests/test_currency.py +0 -0
  150. {cooptools-1.61 → cooptools-1.63}/tests/test_date_utils.py +0 -0
  151. {cooptools-1.61 → cooptools-1.63}/tests/test_fund_projection.py +0 -0
  152. {cooptools-1.61 → cooptools-1.63}/tests/test_os_manip.py +0 -0
  153. {cooptools-1.61 → cooptools-1.63}/tests/test_pandas.py +0 -0
  154. {cooptools-1.61 → cooptools-1.63}/tests/test_printing.py +0 -0
  155. {cooptools-1.61 → cooptools-1.63}/tests/test_qualifiers.py +0 -0
  156. {cooptools-1.61 → cooptools-1.63}/tests/test_sectors.py +0 -0
  157. {cooptools-1.61 → cooptools-1.63}/tests/test_sectors_hex.py +0 -0
  158. {cooptools-1.61 → cooptools-1.63}/tests/test_sectors_sectorTree.py +0 -0
  159. {cooptools-1.61 → cooptools-1.63}/tests/test_statistics.py +0 -0
  160. {cooptools-1.61 → cooptools-1.63}/tests/test_tasks.py +0 -0
  161. {cooptools-1.61 → cooptools-1.63}/tests/test_toggles.py +0 -0
  162. {cooptools-1.61 → cooptools-1.63}/tests/test_trends.py +0 -0
  163. {cooptools-1.61 → cooptools-1.63}/tests/test_typevalidation.py +0 -0
  164. {cooptools-1.61 → cooptools-1.63}/tests/test_vector_utils.py +0 -0
  165. {cooptools-1.61 → cooptools-1.63}/tests/test_version.py +0 -0
  166. {cooptools-1.61 → cooptools-1.63}/tests/tests_graph.py +0 -0
  167. {cooptools-1.61 → cooptools-1.63}/tests/tests_gridsystem.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cooptools
3
- Version: 1.61
3
+ Version: 1.63
4
4
  Summary: Generic Tooling
5
5
  Home-page: https://github.com/tylertjburns/cooptools
6
6
  Author: tburns
@@ -405,8 +405,8 @@ class Graph(object):
405
405
 
406
406
  if isinstance(node, str):
407
407
  node = self.node_by_name(node)
408
-
409
- node_edges = self._node_edge_map[node]
408
+
409
+ node_edges = self._node_edge_map.get(node, [])
410
410
  ret = [self._edges_dict[edge_id] for edge_id in node_edges if self._edges_dict[edge_id].end == node]
411
411
 
412
412
  if only_enabled:
@@ -416,11 +416,11 @@ class Graph(object):
416
416
 
417
417
  def edges_including_node(self, node: Node | str, only_enabled: bool = False, ignored_disablers: List[str] = None) -> List[Edge]:
418
418
  if isinstance(node, str):
419
- node = self.node_by_name(node)
419
+ node = self.node_by_name(node)
420
420
 
421
421
  if not isinstance(node, Node):
422
422
  raise Exception(f"input must be of type {Node}, but {type(node)} was provided")
423
- node_edges = self._node_edge_map[node]
423
+ node_edges = self._node_edge_map.get(node, [])
424
424
  ret = [self._edges_dict[edge_id] for edge_id in node_edges]
425
425
 
426
426
  if only_enabled:
@@ -431,7 +431,7 @@ class Graph(object):
431
431
  def edges_from_node(self, node: Node, only_enabled: bool = False, ignored_disablers: List[str] = None) -> List[Edge]:
432
432
  if not isinstance(node, Node):
433
433
  raise Exception(f"input must be of type {Node}, but {type(node)} was provided")
434
- node_edges = self._node_edge_map[node]
434
+ node_edges = self._node_edge_map.get(node, [])
435
435
  ret = [self._edges_dict[edge_id] for edge_id in node_edges if self._edges_dict[edge_id].start == node]
436
436
 
437
437
  if only_enabled:
@@ -444,7 +444,7 @@ class Graph(object):
444
444
  logger.debug(f"disable {node} with disabler {disabler}")
445
445
 
446
446
  if isinstance(node, Node):
447
- node_edges = self._node_edge_map[node]
447
+ node_edges = self._node_edge_map.get(node, [])
448
448
  for edge_id in node_edges:
449
449
  edge = self._edges_dict[edge_id]
450
450
  edge.add_disabler(disabler)
@@ -453,7 +453,7 @@ class Graph(object):
453
453
  logger.debug(f"enable {node} on disabler {disabler}")
454
454
 
455
455
  if isinstance(node, Node):
456
- node_edges = self._node_edge_map[node]
456
+ node_edges = self._node_edge_map.get(node, [])
457
457
  for edge_id in node_edges:
458
458
  edge = self._edges_dict[edge_id]
459
459
  edge.remove_disabler(disabler)
@@ -662,7 +662,8 @@ class Graph(object):
662
662
  g_func: Callable[[Node, Node], float] = None,
663
663
  h_func: Callable[[Node, Node], float] = None,
664
664
  ignored_disablers:frozenset[str]=None,
665
- disabled_node_ids:frozenset[str] = None) -> AStarResults:
665
+ disabled_node_ids:frozenset[str] = None,
666
+ early_out_cost: float = None) -> AStarResults:
666
667
 
667
668
  if disabled_node_ids is not None:
668
669
  if ((s_in:=(start.name in disabled_node_ids)) or
@@ -672,16 +673,9 @@ class Graph(object):
672
673
  if e_in: msg += f" the end \'{end.name}\' is in the disabled_node_ids"
673
674
  raise Exception(msg)
674
675
 
675
- for x in disabled_node_ids:
676
- self.disable_edges_to_node(
677
- node=self.node_by_name(node_name=x),
678
- disabler=INTERNAL_DISABLER
679
- )
680
-
681
676
  if not ignored_disablers:
682
677
  ignored_disablers = []
683
678
 
684
- """Returns a list of nodes as a path from the given start to the given end in the given graph"""
685
679
  logger.debug(f"Performing A* over map of length: {len(self._graph_dict)}")
686
680
 
687
681
  # Create start and end node
@@ -694,14 +688,17 @@ class Graph(object):
694
688
  open_set = set()
695
689
  closed_set = set()
696
690
 
697
- enabled_connections_to_end = self.edges_to_node(end, only_enabled=True, ignored_disablers=ignored_disablers)
698
- if len(enabled_connections_to_end) != 0:
699
- # Add the start node
691
+ # Check reachability: any enabled edge into end whose source isn't disabled
692
+ connections_to_end = self.edges_to_node(end, only_enabled=True, ignored_disablers=ignored_disablers)
693
+ if disabled_node_ids is not None:
694
+ connections_to_end = [e for e in connections_to_end if e.start.name not in disabled_node_ids]
695
+ if len(connections_to_end) != 0:
700
696
  open_set.add(start_iter)
701
697
 
702
698
  cc = -1
703
699
 
704
700
  results = None
701
+ _early_out_triggered = False
705
702
  # Loop until you find the end
706
703
  while len(open_set) > 0:
707
704
  cc += 1
@@ -712,6 +709,13 @@ class Graph(object):
712
709
  if open_item.f < current_item.f:
713
710
  current_item = open_item
714
711
 
712
+ # Early-out: f is a lower bound on total path cost through current_item.
713
+ # If the minimum f in the open set already meets or exceeds the caller's
714
+ # cost ceiling, no reachable path can improve on what's already known.
715
+ if early_out_cost is not None and current_item.f >= early_out_cost:
716
+ _early_out_triggered = True
717
+ break
718
+
715
719
  steps[cc] = {"open_set": set(open_set), "closed_set": set(closed_set), "current_item": current_item}
716
720
 
717
721
  # Pop current off open list, add to closed list
@@ -733,12 +737,15 @@ class Graph(object):
733
737
  steps,
734
738
  source=start,
735
739
  dest=end,
736
- disabled_node_ids=list(disabled_node_ids) if disabled_node_ids is not None else None) # Return reversed path
740
+ disabled_node_ids=list(disabled_node_ids) if disabled_node_ids is not None else None)
737
741
  break
738
742
 
739
743
  # Generate children
740
744
  for new_node in self._graph_dict[current_item.graph_node]: # Adjacent nodes
741
- # edges
745
+ # Skip nodes excluded by the caller — checked inline to avoid mutating the graph
746
+ if disabled_node_ids is not None and new_node.name in disabled_node_ids:
747
+ continue
748
+
742
749
  edges = self.edges_between(
743
750
  nodeA=current_item.graph_node,
744
751
  nodeB=new_node
@@ -749,8 +756,8 @@ class Graph(object):
749
756
  if len(enabled_edges) == 0:
750
757
  continue
751
758
 
752
- # find the min-lenght edge
753
- enabled_edges.sort(key=lambda x: x.length)
759
+ # find the min-cost edge
760
+ enabled_edges.sort(key=lambda x: x.cost)
754
761
  min_edge = enabled_edges[0]
755
762
 
756
763
  new_item = AStarMetrics(current_item, new_node, min_edge)
@@ -761,8 +768,7 @@ class Graph(object):
761
768
  open_set.add(new_item)
762
769
 
763
770
  # calculate new g, h, f from current pivot node to the new node
764
- calc_g = current_item.g + g_func(current_item.graph_node,
765
- new_node) if g_func else vec.distance_between(new_node.pos, current_item.graph_node.pos)
771
+ calc_g = current_item.g + (g_func(current_item.graph_node, new_node) if g_func else min_edge.cost)
766
772
  calc_h = h_func(new_node,
767
773
  end_iter.graph_node) if h_func else vec.distance_between(new_node.pos, end_iter.graph_node.pos)
768
774
  calc_f = calc_g + calc_h
@@ -773,28 +779,25 @@ class Graph(object):
773
779
  new_item.f = calc_f
774
780
 
775
781
  if results is None:
776
- ''' No Path Found '''
777
- logger.error(f"Unable to find a path from [{start}] to [{end}]")
782
+ if _early_out_triggered:
783
+ logger.debug(f"A* early-out from [{start}] to [{end}]: f >= early_out_cost ({early_out_cost})")
784
+ failure_reason = RouteFailureReason.EARLY_OUT_REACHED
785
+ else:
786
+ logger.error(f"Unable to find a path from [{start}] to [{end}]")
787
+ failure_reason = RouteFailureReason.ROUTE_NOT_POSSIBLE
778
788
 
779
789
  ret = AStarResults([],
780
790
  [],
781
791
  steps,
782
792
  source=start,
783
793
  dest=end,
784
- disabled_node_ids=list(disabled_node_ids) if disabled_node_ids is not None else None # Return reversed path
794
+ disabled_node_ids=list(disabled_node_ids) if disabled_node_ids is not None else None,
795
+ failure_reason=failure_reason,
785
796
  )
786
797
  else:
787
- ''' Log Path found '''
788
798
  logger.debug(f"Path found from [{start}] to [{end}] in {len(steps)} steps")
789
799
  ret = results
790
800
 
791
- if disabled_node_ids is not None:
792
- for x in disabled_node_ids:
793
- self.enable_edges_to_node(
794
- node=self.node_by_name(node_name=x),
795
- disabler=INTERNAL_DISABLER
796
- )
797
-
798
801
  return ret
799
802
 
800
803
  def path_length(self, path:List[Node]):
@@ -890,67 +893,336 @@ class Graph(object):
890
893
  def branching_target_seek(self,
891
894
  sources: Iterable[Node],
892
895
  targets: Iterable[Node],
893
- disabled_node_ids:frozenset[str] = None):
896
+ disabled_node_ids: frozenset[str] = None) -> Tuple[Self, list[dict]]:
897
+ """Grow a spanning tree from sources toward all targets one step at a time.
898
+
899
+ Returns:
900
+ Tuple of (Graph, steps) where steps is a list of dicts capturing
901
+ algorithm state after each phase of each iteration.
902
+ """
894
903
  if disabled_node_ids is not None:
895
904
  for x in disabled_node_ids:
896
905
  self.disable_edges_to_node(
897
906
  node=self.node_by_name(node_name=x),
898
907
  disabler=INTERNAL_DISABLER
899
908
  )
900
-
901
- active_nodes: set[Node] = set(sources)
902
- current_targets: set[Node] = set(targets)
903
- branches_graph_dict = {
904
- x: [] for x in sources
905
- }
906
909
 
907
- routes = {}
910
+ source_set: set[Node] = set(sources)
911
+ active_nodes: set[Node] = set(sources)
912
+ current_targets: set[Node] = set(targets)
913
+ branches_graph_dict: Dict[Node, list] = {x: [] for x in sources}
914
+ in_tree: set[Node] = set(sources)
915
+ parent_map: Dict[Node, Node] = {}
916
+ reached_targets: set[Node] = set() # targets already reached — never prune these
917
+
918
+ steps: list[dict] = []
919
+
920
+ def _snap(label, new_edges=None, best_routes=None, pruned=None):
921
+ steps.append({
922
+ "label": label,
923
+ "active": frozenset(active_nodes),
924
+ "targets": frozenset(current_targets),
925
+ "in_tree": frozenset(in_tree),
926
+ "branches": {k: list(v) for k, v in branches_graph_dict.items()},
927
+ "new_edges": list(new_edges or []), # list of (Node, Node) pairs
928
+ "best_routes": list(best_routes or []), # list of Node sequences
929
+ "pruned": list(pruned or []), # list of Nodes removed this step
930
+ })
931
+
932
+ _snap("Initial state")
908
933
 
909
934
  while len(current_targets) > 0:
910
935
  target_nearest: Dict[Node, AStarResults] = {}
911
936
 
912
- # Calculate routes from active nodes to targets
937
+ # Calculate cheapest route from any active node to each target
913
938
  for node in active_nodes:
914
939
  for target in current_targets:
915
- route = self.astar(
916
- start=node,
917
- end=target
918
- )
919
-
940
+ route = self.astar(start=node, end=target)
920
941
  target_nearest.setdefault(target, None)
921
- if route.nodes is not None and len(route.nodes) > 0 and (target_nearest.get(target) is None or route.Cost < target_nearest[target].Cost):
942
+ if route.nodes is not None and len(route.nodes) > 0 and (
943
+ target_nearest.get(target) is None or
944
+ route.Cost < target_nearest[target].Cost
945
+ ):
922
946
  target_nearest[target] = route
923
947
 
924
- # progress the branches
948
+ best_routes_display = [
949
+ list(r.nodes) for r in target_nearest.values()
950
+ if r is not None and r.nodes is not None and len(r.nodes) > 0
951
+ ]
952
+ _snap("Routes found", best_routes=best_routes_display)
953
+
954
+ # Identify which active nodes hold at least one best route this iteration
955
+ contributing: set[Node] = set()
956
+ for route in target_nearest.values():
957
+ if route is not None and route.nodes is not None and len(route.nodes) > 0:
958
+ contributing.add(list(route.nodes)[0])
959
+
960
+ # Prune dominated active nodes (hold no best route) back to the last branch.
961
+ # Contributing nodes advance monotonically closer to remaining targets, so a
962
+ # dominated node will never win a future iteration.
963
+ # Reached targets are exempt — they stay active so the tree can extend through
964
+ # them toward further targets.
965
+ pruned_this_step: list[Node] = []
966
+ dominated = active_nodes - contributing - reached_targets
967
+ for dominated_node in dominated:
968
+ current = dominated_node
969
+ while current not in source_set:
970
+ parent = parent_map.get(current)
971
+ active_nodes.discard(current)
972
+ in_tree.discard(current)
973
+ branches_graph_dict.pop(current, None)
974
+ parent_map.pop(current, None)
975
+ if parent is not None:
976
+ children = branches_graph_dict.get(parent, [])
977
+ if current in children:
978
+ children.remove(current)
979
+ pruned_this_step.append(current)
980
+ if parent is None or parent in source_set:
981
+ break
982
+ if branches_graph_dict.get(parent, []):
983
+ break # parent still has other children — real branch node
984
+ if parent in contributing:
985
+ active_nodes.add(parent) # restore contributing parent to active
986
+ break
987
+ current = parent # cascade prune
988
+
989
+ if pruned_this_step:
990
+ _snap("Pruned dominated nodes", pruned=pruned_this_step)
991
+
992
+ # Progress the branches one step toward each target
993
+ new_edges: list[tuple[Node, Node]] = []
925
994
  for target, best_route in target_nearest.items():
926
- if best_route.nodes is None or len(best_route.nodes) == 0:
927
- current_targets.remove(target)
995
+ if target not in current_targets:
928
996
  continue
929
997
 
930
- from_node = list(best_route.nodes)[0]
998
+ if best_route is None or best_route.nodes is None or len(best_route.nodes) == 0:
999
+ current_targets.discard(target)
1000
+ continue
1001
+
1002
+ from_node = list(best_route.nodes)[0]
1003
+ first_visit = list(best_route.nodes)[1]
931
1004
 
932
1005
  if from_node in active_nodes:
933
1006
  active_nodes.remove(from_node)
934
1007
 
935
- # skip the node where started, and grab the next visited
936
- first_visit = list(best_route.nodes)[1]
1008
+ if first_visit not in in_tree:
1009
+ branches_graph_dict[from_node].append(first_visit)
1010
+ branches_graph_dict.setdefault(first_visit, [])
1011
+ active_nodes.add(first_visit)
1012
+ in_tree.add(first_visit)
1013
+ parent_map[first_visit] = from_node
1014
+ new_edges.append((from_node, first_visit))
937
1015
 
938
- # build the branch
939
- branches_graph_dict[from_node].append(first_visit)
940
- branches_graph_dict.setdefault(first_visit, [])
1016
+ if first_visit in current_targets:
1017
+ current_targets.discard(first_visit)
1018
+ reached_targets.add(first_visit)
941
1019
 
942
- # add new node to eligible active nodes
943
- active_nodes.add(first_visit)
1020
+ _snap("Edges added", new_edges=new_edges, best_routes=best_routes_display)
944
1021
 
945
- # if have reached a target, remove from targets
946
- if first_visit in current_targets:
947
- current_targets.remove(first_visit)
1022
+ if disabled_node_ids is not None:
1023
+ for x in disabled_node_ids:
1024
+ self.enable_edges_to_node(
1025
+ node=self.node_by_name(node_name=x),
1026
+ disabler=INTERNAL_DISABLER
1027
+ )
1028
+
1029
+ for src in list(source_set):
1030
+ if branches_graph_dict.get(src) == []:
1031
+ branches_graph_dict.pop(src, None)
1032
+ return Graph(branches_graph_dict), steps
1033
+
1034
+ def greedy_target_seek(self,
1035
+ sources: Iterable[Node],
1036
+ targets: Iterable[Node],
1037
+ disabled_node_ids: frozenset[str] = None) -> Tuple[Self, list[dict]]:
1038
+ """Grow a spanning tree from sources toward all targets using greedy path commitment.
1039
+
1040
+ Unlike branching_target_seek (which advances one hop per outer iteration),
1041
+ this function advances each frontier along its full planned A* path until it
1042
+ collides with a node already committed to the tree. Cheaper paths commit
1043
+ first, so shared trunk segments are claimed before branches check for
1044
+ collisions — this produces the correct branching topology in O(branch_depth)
1045
+ outer iterations instead of O(max_path_length).
1046
+
1047
+ Returns:
1048
+ Tuple of (Graph, steps) mirroring branching_target_seek's interface.
1049
+ """
1050
+ if disabled_node_ids is not None:
1051
+ for x in disabled_node_ids:
1052
+ self.disable_edges_to_node(
1053
+ node=self.node_by_name(node_name=x),
1054
+ disabler=INTERNAL_DISABLER
1055
+ )
1056
+
1057
+ source_set: set[Node] = set(sources)
1058
+ active_nodes: set[Node] = set(sources)
1059
+ current_targets: set[Node] = set(targets)
1060
+ branches_graph_dict: Dict[Node, list] = {x: [] for x in sources}
1061
+ in_tree: set[Node] = set(sources)
1062
+ parent_map: Dict[Node, Node] = {}
1063
+ reached_targets: set[Node] = set()
1064
+
1065
+ steps: list[dict] = []
1066
+
1067
+ def _snap(label, new_edges=None, best_routes=None, pruned=None):
1068
+ steps.append({
1069
+ "label": label,
1070
+ "active": frozenset(active_nodes),
1071
+ "targets": frozenset(current_targets),
1072
+ "in_tree": frozenset(in_tree),
1073
+ "branches": {k: list(v) for k, v in branches_graph_dict.items()},
1074
+ "new_edges": list(new_edges or []),
1075
+ "best_routes": list(best_routes or []),
1076
+ "pruned": list(pruned or []),
1077
+ })
1078
+
1079
+ _snap("Initial state")
1080
+
1081
+ while len(current_targets) > 0:
1082
+ target_nearest: Dict[Node, AStarResults] = {}
1083
+
1084
+ # Find the best full A* path from any active node to each target
1085
+ for node in active_nodes:
1086
+ for target in current_targets:
1087
+ route = self.astar(start=node, end=target)
1088
+ target_nearest.setdefault(target, None)
1089
+ if route.nodes is not None and len(route.nodes) > 0 and (
1090
+ target_nearest.get(target) is None or
1091
+ route.Cost < target_nearest[target].Cost
1092
+ ):
1093
+ target_nearest[target] = route
1094
+
1095
+ # Discard targets with no reachable path
1096
+ for target in list(current_targets):
1097
+ route = target_nearest.get(target)
1098
+ if route is None or not route.nodes or len(route.nodes) < 2:
1099
+ current_targets.discard(target)
1100
+
1101
+ if not current_targets:
1102
+ break
1103
+
1104
+ best_routes_display = [
1105
+ list(r.nodes) for r in target_nearest.values()
1106
+ if r is not None and r.nodes is not None and len(r.nodes) > 0
1107
+ ]
1108
+ _snap("Routes found", best_routes=best_routes_display)
1109
+
1110
+ # Identify which active nodes hold at least one best route this iteration
1111
+ contributing: set[Node] = set()
1112
+ for target, route in target_nearest.items():
1113
+ if target in current_targets and route is not None and route.nodes:
1114
+ contributing.add(list(route.nodes)[0])
1115
+
1116
+ # Prune dominated active nodes back to the last branch.
1117
+ # Reached targets are exempt — they stay active so the tree can
1118
+ # extend through them toward further targets.
1119
+ pruned_this_step: list[Node] = []
1120
+ dominated = active_nodes - contributing - reached_targets
1121
+ for dominated_node in dominated:
1122
+ current = dominated_node
1123
+ while current not in source_set:
1124
+ parent = parent_map.get(current)
1125
+ active_nodes.discard(current)
1126
+ in_tree.discard(current)
1127
+ branches_graph_dict.pop(current, None)
1128
+ parent_map.pop(current, None)
1129
+ if parent is not None:
1130
+ children = branches_graph_dict.get(parent, [])
1131
+ if current in children:
1132
+ children.remove(current)
1133
+ pruned_this_step.append(current)
1134
+ if parent is None or parent in source_set:
1135
+ break
1136
+ if branches_graph_dict.get(parent, []):
1137
+ break
1138
+ if parent in contributing:
1139
+ active_nodes.add(parent)
1140
+ break
1141
+ current = parent
1142
+
1143
+ if pruned_this_step:
1144
+ _snap("Pruned dominated nodes", pruned=pruned_this_step)
1145
+
1146
+ # Advance each frontier along its full planned path until it collides
1147
+ # with a node already in the tree. Cheaper paths commit first.
1148
+ reachable = [
1149
+ (t, r) for t, r in target_nearest.items()
1150
+ if t in current_targets and r is not None and r.nodes and len(r.nodes) >= 2
1151
+ ]
1152
+ reachable.sort(key=lambda x: x[1].Cost)
1153
+
1154
+ new_edges: list[tuple[Node, Node]] = []
1155
+
1156
+ for target, best_route in reachable:
1157
+ if target not in current_targets:
1158
+ continue
1159
+
1160
+ path_nodes = list(best_route.nodes)
1161
+ from_node = path_nodes[0]
1162
+ active_nodes.discard(from_node)
1163
+
1164
+ prev = from_node
1165
+ new_frontier = from_node
1166
+ left_shared_prefix = False
1167
+
1168
+ for node in path_nodes[1:]:
1169
+ if node in in_tree:
1170
+ if not left_shared_prefix:
1171
+ # Fast-forward through the shared trunk without committing.
1172
+ # The trunk is already in the tree; we just find where our
1173
+ # path diverges from it.
1174
+ prev = node
1175
+ new_frontier = node
1176
+ else:
1177
+ # Our new branch segment reconnects to an existing tree node
1178
+ # (confluence / tributary join). Wire it in and stop.
1179
+ branches_graph_dict.setdefault(prev, [])
1180
+ if node not in branches_graph_dict[prev]:
1181
+ branches_graph_dict[prev].append(node)
1182
+ new_frontier = node
1183
+ break
1184
+ else:
1185
+ left_shared_prefix = True
1186
+ branches_graph_dict.setdefault(prev, [])
1187
+ if node not in branches_graph_dict[prev]:
1188
+ branches_graph_dict[prev].append(node)
1189
+ branches_graph_dict.setdefault(node, [])
1190
+ in_tree.add(node)
1191
+ parent_map[node] = prev
1192
+ new_edges.append((prev, node))
1193
+ prev = node
1194
+ new_frontier = node
1195
+ if node == target:
1196
+ break
1197
+
1198
+ active_nodes.add(new_frontier)
1199
+
1200
+ if new_frontier == target or new_frontier in current_targets:
1201
+ current_targets.discard(new_frontier)
1202
+ current_targets.discard(target)
1203
+ reached_targets.add(new_frontier)
1204
+
1205
+ # Post-commit sweep: a path's walk may have passed through another target's
1206
+ # node en route to its own target. Mark any such targets as reached now.
1207
+ for t in list(current_targets):
1208
+ if t in in_tree:
1209
+ current_targets.discard(t)
1210
+ reached_targets.add(t)
1211
+ active_nodes.discard(t)
1212
+
1213
+ _snap("Edges added", new_edges=new_edges, best_routes=best_routes_display)
1214
+
1215
+ if disabled_node_ids is not None:
1216
+ for x in disabled_node_ids:
1217
+ self.enable_edges_to_node(
1218
+ node=self.node_by_name(node_name=x),
1219
+ disabler=INTERNAL_DISABLER
1220
+ )
948
1221
 
949
- # return
950
- for src in list(sources):
1222
+ for src in list(source_set):
951
1223
  if branches_graph_dict.get(src) == []:
952
1224
  branches_graph_dict.pop(src, None)
953
- return Graph(branches_graph_dict)
1225
+ return Graph(branches_graph_dict), steps
954
1226
 
955
1227
  @property
956
1228
  def SectorGraph(self) -> Self:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cooptools
3
- Version: 1.61
3
+ Version: 1.63
4
4
  Summary: Generic Tooling
5
5
  Home-page: https://github.com/tylertjburns/cooptools
6
6
  Author: tburns
@@ -7,7 +7,7 @@ with open('requirements.txt') as f:
7
7
  requirements = f.read().splitlines()
8
8
 
9
9
  setuptools.setup(name='cooptools',
10
- version='1.61',
10
+ version='1.63',
11
11
  description='Generic Tooling',
12
12
  url='https://github.com/tylertjburns/cooptools',
13
13
  author='tburns',
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
File without changes