cooptools 1.62__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.62 → cooptools-1.63}/PKG-INFO +1 -1
  2. {cooptools-1.62 → cooptools-1.63}/cooptools/graphs/graph.py +205 -22
  3. {cooptools-1.62 → cooptools-1.63}/cooptools.egg-info/PKG-INFO +1 -1
  4. {cooptools-1.62 → cooptools-1.63}/setup.py +1 -1
  5. {cooptools-1.62 → cooptools-1.63}/MANIFEST.in +0 -0
  6. {cooptools-1.62 → cooptools-1.63}/README.md +0 -0
  7. {cooptools-1.62 → cooptools-1.63}/cooptools/__init__.py +0 -0
  8. {cooptools-1.62 → cooptools-1.63}/cooptools/anchor.py +0 -0
  9. {cooptools-1.62 → cooptools-1.63}/cooptools/asyncable.py +0 -0
  10. {cooptools-1.62 → cooptools-1.63}/cooptools/cli/CliAtomicUserInteraction.py +0 -0
  11. {cooptools-1.62 → cooptools-1.63}/cooptools/cli/CliMenu.py +0 -0
  12. {cooptools-1.62 → cooptools-1.63}/cooptools/cli/__init__.py +0 -0
  13. {cooptools-1.62 → cooptools-1.63}/cooptools/cli/fileContentReturn.py +0 -0
  14. {cooptools-1.62 → cooptools-1.63}/cooptools/cnxn_info.py +0 -0
  15. {cooptools-1.62 → cooptools-1.63}/cooptools/colors.py +0 -0
  16. {cooptools-1.62 → cooptools-1.63}/cooptools/commandDesignPattern/__init__.py +0 -0
  17. {cooptools-1.62 → cooptools-1.63}/cooptools/commandDesignPattern/commandController.py +0 -0
  18. {cooptools-1.62 → cooptools-1.63}/cooptools/commandDesignPattern/commandProtocol.py +0 -0
  19. {cooptools-1.62 → cooptools-1.63}/cooptools/commandDesignPattern/exceptions.py +0 -0
  20. {cooptools-1.62 → cooptools-1.63}/cooptools/common.py +0 -0
  21. {cooptools-1.62 → cooptools-1.63}/cooptools/config.py +0 -0
  22. {cooptools-1.62 → cooptools-1.63}/cooptools/coopDataclass.py +0 -0
  23. {cooptools-1.62 → cooptools-1.63}/cooptools/coopEnum.py +0 -0
  24. {cooptools-1.62 → cooptools-1.63}/cooptools/coopthreading.py +0 -0
  25. {cooptools-1.62 → cooptools-1.63}/cooptools/currency.py +0 -0
  26. {cooptools-1.62 → cooptools-1.63}/cooptools/dataRefresher/__init__.py +0 -0
  27. {cooptools-1.62 → cooptools-1.63}/cooptools/dataRefresher/dataHub.py +0 -0
  28. {cooptools-1.62 → cooptools-1.63}/cooptools/dataRefresher/dataRefresher.py +0 -0
  29. {cooptools-1.62 → cooptools-1.63}/cooptools/dataStore/__init__.py +0 -0
  30. {cooptools-1.62 → cooptools-1.63}/cooptools/dataStore/dataProcessor.py +0 -0
  31. {cooptools-1.62 → cooptools-1.63}/cooptools/dataStore/dataStoreProtocol.py +0 -0
  32. {cooptools-1.62 → cooptools-1.63}/cooptools/dataStore/dbConnectionURI.py +0 -0
  33. {cooptools-1.62 → cooptools-1.63}/cooptools/dataStore/inMemoryDataStore.py +0 -0
  34. {cooptools-1.62 → cooptools-1.63}/cooptools/date_utils.py +0 -0
  35. {cooptools-1.62 → cooptools-1.63}/cooptools/decay.py +0 -0
  36. {cooptools-1.62 → cooptools-1.63}/cooptools/decor.py +0 -0
  37. {cooptools-1.62 → cooptools-1.63}/cooptools/dictPolicies.py +0 -0
  38. {cooptools-1.62 → cooptools-1.63}/cooptools/exceptions.py +0 -0
  39. {cooptools-1.62 → cooptools-1.63}/cooptools/expertise/__init__.py +0 -0
  40. {cooptools-1.62 → cooptools-1.63}/cooptools/expertise/expertiseArgs.py +0 -0
  41. {cooptools-1.62 → cooptools-1.63}/cooptools/expertise/expertiseSchedules.py +0 -0
  42. {cooptools-1.62 → cooptools-1.63}/cooptools/finance/__init__.py +0 -0
  43. {cooptools-1.62 → cooptools-1.63}/cooptools/finance/dicounted_cashflow_analysis.py +0 -0
  44. {cooptools-1.62 → cooptools-1.63}/cooptools/finance/fund_projection.py +0 -0
  45. {cooptools-1.62 → cooptools-1.63}/cooptools/finance/fund_projection_renderer.py +0 -0
  46. {cooptools-1.62 → cooptools-1.63}/cooptools/finance/futureValueProjector.py +0 -0
  47. {cooptools-1.62 → cooptools-1.63}/cooptools/finance/growth_projections.py +0 -0
  48. {cooptools-1.62 → cooptools-1.63}/cooptools/finance/reports.py +0 -0
  49. {cooptools-1.62 → cooptools-1.63}/cooptools/finance/utils.py +0 -0
  50. {cooptools-1.62 → cooptools-1.63}/cooptools/geometry_utils/__init__.py +0 -0
  51. {cooptools-1.62 → cooptools-1.63}/cooptools/geometry_utils/circle_utils.py +0 -0
  52. {cooptools-1.62 → cooptools-1.63}/cooptools/geometry_utils/common.py +0 -0
  53. {cooptools-1.62 → cooptools-1.63}/cooptools/geometry_utils/curve_utils.py +0 -0
  54. {cooptools-1.62 → cooptools-1.63}/cooptools/geometry_utils/line_utils.py +0 -0
  55. {cooptools-1.62 → cooptools-1.63}/cooptools/geometry_utils/polygon_utils.py +0 -0
  56. {cooptools-1.62 → cooptools-1.63}/cooptools/geometry_utils/rect_utils.py +0 -0
  57. {cooptools-1.62 → cooptools-1.63}/cooptools/geometry_utils/triangle_utils.py +0 -0
  58. {cooptools-1.62 → cooptools-1.63}/cooptools/geometry_utils/vector_utils.py +0 -0
  59. {cooptools-1.62 → cooptools-1.63}/cooptools/graphs/__init__.py +0 -0
  60. {cooptools-1.62 → cooptools-1.63}/cooptools/graphs/astar_results_viewer.py +0 -0
  61. {cooptools-1.62 → cooptools-1.63}/cooptools/graphs/draw.py +0 -0
  62. {cooptools-1.62 → cooptools-1.63}/cooptools/graphs/graph_dcs.py +0 -0
  63. {cooptools-1.62 → cooptools-1.63}/cooptools/graphs/graph_definitions.py +0 -0
  64. {cooptools-1.62 → cooptools-1.63}/cooptools/graphs/utils.py +0 -0
  65. {cooptools-1.62 → cooptools-1.63}/cooptools/ideas/__init__.py +0 -0
  66. {cooptools-1.62 → cooptools-1.63}/cooptools/ideas/actionItemTracker/__init__.py +0 -0
  67. {cooptools-1.62 → cooptools-1.63}/cooptools/ideas/actionItemTracker/ai_tracker.py +0 -0
  68. {cooptools-1.62 → cooptools-1.63}/cooptools/ideas/actionItemTracker/dcs.py +0 -0
  69. {cooptools-1.62 → cooptools-1.63}/cooptools/ideas/discreteEventSimulator/__init__.py +0 -0
  70. {cooptools-1.62 → cooptools-1.63}/cooptools/ideas/discreteEventSimulator/discreteEventSimulator.py +0 -0
  71. {cooptools-1.62 → cooptools-1.63}/cooptools/ideas/jsonIO.py +0 -0
  72. {cooptools-1.62 → cooptools-1.63}/cooptools/ideas/metrics.py +0 -0
  73. {cooptools-1.62 → cooptools-1.63}/cooptools/ideas/triggerActionSystem/__init__.py +0 -0
  74. {cooptools-1.62 → cooptools-1.63}/cooptools/ideas/triggerActionSystem/signal.py +0 -0
  75. {cooptools-1.62 → cooptools-1.63}/cooptools/ideas/triggerActionSystem/trigger.py +0 -0
  76. {cooptools-1.62 → cooptools-1.63}/cooptools/loggingHelpers.py +0 -0
  77. {cooptools-1.62 → cooptools-1.63}/cooptools/marchingSquares.py +0 -0
  78. {cooptools-1.62 → cooptools-1.63}/cooptools/materialHandling/__init__.py +0 -0
  79. {cooptools-1.62 → cooptools-1.63}/cooptools/materialHandling/cli.py +0 -0
  80. {cooptools-1.62 → cooptools-1.63}/cooptools/materialHandling/dcs.py +0 -0
  81. {cooptools-1.62 → cooptools-1.63}/cooptools/materialHandling/selectionCriteria.py +0 -0
  82. {cooptools-1.62 → cooptools-1.63}/cooptools/matrixManipulation.py +0 -0
  83. {cooptools-1.62 → cooptools-1.63}/cooptools/os_manip.py +0 -0
  84. {cooptools-1.62 → cooptools-1.63}/cooptools/pandasHelpers.py +0 -0
  85. {cooptools-1.62 → cooptools-1.63}/cooptools/physics/__init__.py +0 -0
  86. {cooptools-1.62 → cooptools-1.63}/cooptools/physics/kinematic.py +0 -0
  87. {cooptools-1.62 → cooptools-1.63}/cooptools/physics/kinematic_utils.py +0 -0
  88. {cooptools-1.62 → cooptools-1.63}/cooptools/physics/movement.py +0 -0
  89. {cooptools-1.62 → cooptools-1.63}/cooptools/physics/trajectory.py +0 -0
  90. {cooptools-1.62 → cooptools-1.63}/cooptools/physics/waypoint.py +0 -0
  91. {cooptools-1.62 → cooptools-1.63}/cooptools/physics/waypointFollower.py +0 -0
  92. {cooptools-1.62 → cooptools-1.63}/cooptools/plotting.py +0 -0
  93. {cooptools-1.62 → cooptools-1.63}/cooptools/printing.py +0 -0
  94. {cooptools-1.62 → cooptools-1.63}/cooptools/protocols.py +0 -0
  95. {cooptools-1.62 → cooptools-1.63}/cooptools/qualifiers/__init__.py +0 -0
  96. {cooptools-1.62 → cooptools-1.63}/cooptools/qualifiers/cli.py +0 -0
  97. {cooptools-1.62 → cooptools-1.63}/cooptools/qualifiers/qualifier.py +0 -0
  98. {cooptools-1.62 → cooptools-1.63}/cooptools/randoms.py +0 -0
  99. {cooptools-1.62 → cooptools-1.63}/cooptools/register.py +0 -0
  100. {cooptools-1.62 → cooptools-1.63}/cooptools/reservation/__init__.py +0 -0
  101. {cooptools-1.62 → cooptools-1.63}/cooptools/reservation/dcs.py +0 -0
  102. {cooptools-1.62 → cooptools-1.63}/cooptools/reservation/enums.py +0 -0
  103. {cooptools-1.62 → cooptools-1.63}/cooptools/reservation/reservationmanager.py +0 -0
  104. {cooptools-1.62 → cooptools-1.63}/cooptools/retry.py +0 -0
  105. {cooptools-1.62 → cooptools-1.63}/cooptools/sectors/__init__.py +0 -0
  106. {cooptools-1.62 → cooptools-1.63}/cooptools/sectors/grids/__init__.py +0 -0
  107. {cooptools-1.62 → cooptools-1.63}/cooptools/sectors/grids/gridState.py +0 -0
  108. {cooptools-1.62 → cooptools-1.63}/cooptools/sectors/grids/grid_base.py +0 -0
  109. {cooptools-1.62 → cooptools-1.63}/cooptools/sectors/grids/hexGrid.py +0 -0
  110. {cooptools-1.62 → cooptools-1.63}/cooptools/sectors/grids/rectGrid.py +0 -0
  111. {cooptools-1.62 → cooptools-1.63}/cooptools/sectors/hex_utils.py +0 -0
  112. {cooptools-1.62 → cooptools-1.63}/cooptools/sectors/sect_utils.py +0 -0
  113. {cooptools-1.62 → cooptools-1.63}/cooptools/sectors/sectorTree/__init__.py +0 -0
  114. {cooptools-1.62 → cooptools-1.63}/cooptools/sectors/sectorTree/sectorTree.py +0 -0
  115. {cooptools-1.62 → cooptools-1.63}/cooptools/statistics/__init__.py +0 -0
  116. {cooptools-1.62 → cooptools-1.63}/cooptools/statistics/activityReport.py +0 -0
  117. {cooptools-1.62 → cooptools-1.63}/cooptools/statistics/controlChart/__init__.py +0 -0
  118. {cooptools-1.62 → cooptools-1.63}/cooptools/statistics/controlChart/controlChart.py +0 -0
  119. {cooptools-1.62 → cooptools-1.63}/cooptools/statistics/controlChart/plotting.py +0 -0
  120. {cooptools-1.62 → cooptools-1.63}/cooptools/statistics/oee/__init__.py +0 -0
  121. {cooptools-1.62 → cooptools-1.63}/cooptools/statistics/oee/oeeHub.py +0 -0
  122. {cooptools-1.62 → cooptools-1.63}/cooptools/taskProcessing/__init__.py +0 -0
  123. {cooptools-1.62 → cooptools-1.63}/cooptools/taskProcessing/dcs.py +0 -0
  124. {cooptools-1.62 → cooptools-1.63}/cooptools/taskProcessing/taskProcessor.py +0 -0
  125. {cooptools-1.62 → cooptools-1.63}/cooptools/timeTracker/__init__.py +0 -0
  126. {cooptools-1.62 → cooptools-1.63}/cooptools/timeTracker/decay.py +0 -0
  127. {cooptools-1.62 → cooptools-1.63}/cooptools/timeTracker/timeTracker.py +0 -0
  128. {cooptools-1.62 → cooptools-1.63}/cooptools/timeWindow.py +0 -0
  129. {cooptools-1.62 → cooptools-1.63}/cooptools/toggles.py +0 -0
  130. {cooptools-1.62 → cooptools-1.63}/cooptools/transform.py +0 -0
  131. {cooptools-1.62 → cooptools-1.63}/cooptools/trends.py +0 -0
  132. {cooptools-1.62 → cooptools-1.63}/cooptools/typeProviders.py +0 -0
  133. {cooptools-1.62 → cooptools-1.63}/cooptools/typevalidation.py +0 -0
  134. {cooptools-1.62 → cooptools-1.63}/cooptools/version.py +0 -0
  135. {cooptools-1.62 → cooptools-1.63}/cooptools.egg-info/SOURCES.txt +0 -0
  136. {cooptools-1.62 → cooptools-1.63}/cooptools.egg-info/dependency_links.txt +0 -0
  137. {cooptools-1.62 → cooptools-1.63}/cooptools.egg-info/not-zip-safe +0 -0
  138. {cooptools-1.62 → cooptools-1.63}/cooptools.egg-info/requires.txt +0 -0
  139. {cooptools-1.62 → cooptools-1.63}/cooptools.egg-info/top_level.txt +0 -0
  140. {cooptools-1.62 → cooptools-1.63}/requirements.txt +0 -0
  141. {cooptools-1.62 → cooptools-1.63}/setup.cfg +0 -0
  142. {cooptools-1.62 → cooptools-1.63}/tests/test_CliAtomicUserInteraction.py +0 -0
  143. {cooptools-1.62 → cooptools-1.63}/tests/test_asyncable.py +0 -0
  144. {cooptools-1.62 → cooptools-1.63}/tests/test_cdp_bugs.py +0 -0
  145. {cooptools-1.62 → cooptools-1.63}/tests/test_commandDesignPattern.py +0 -0
  146. {cooptools-1.62 → cooptools-1.63}/tests/test_common.py +0 -0
  147. {cooptools-1.62 → cooptools-1.63}/tests/test_config.py +0 -0
  148. {cooptools-1.62 → cooptools-1.63}/tests/test_coopEnum.py +0 -0
  149. {cooptools-1.62 → cooptools-1.63}/tests/test_currency.py +0 -0
  150. {cooptools-1.62 → cooptools-1.63}/tests/test_date_utils.py +0 -0
  151. {cooptools-1.62 → cooptools-1.63}/tests/test_fund_projection.py +0 -0
  152. {cooptools-1.62 → cooptools-1.63}/tests/test_os_manip.py +0 -0
  153. {cooptools-1.62 → cooptools-1.63}/tests/test_pandas.py +0 -0
  154. {cooptools-1.62 → cooptools-1.63}/tests/test_printing.py +0 -0
  155. {cooptools-1.62 → cooptools-1.63}/tests/test_qualifiers.py +0 -0
  156. {cooptools-1.62 → cooptools-1.63}/tests/test_sectors.py +0 -0
  157. {cooptools-1.62 → cooptools-1.63}/tests/test_sectors_hex.py +0 -0
  158. {cooptools-1.62 → cooptools-1.63}/tests/test_sectors_sectorTree.py +0 -0
  159. {cooptools-1.62 → cooptools-1.63}/tests/test_statistics.py +0 -0
  160. {cooptools-1.62 → cooptools-1.63}/tests/test_tasks.py +0 -0
  161. {cooptools-1.62 → cooptools-1.63}/tests/test_toggles.py +0 -0
  162. {cooptools-1.62 → cooptools-1.63}/tests/test_trends.py +0 -0
  163. {cooptools-1.62 → cooptools-1.63}/tests/test_typevalidation.py +0 -0
  164. {cooptools-1.62 → cooptools-1.63}/tests/test_vector_utils.py +0 -0
  165. {cooptools-1.62 → cooptools-1.63}/tests/test_version.py +0 -0
  166. {cooptools-1.62 → cooptools-1.63}/tests/tests_graph.py +0 -0
  167. {cooptools-1.62 → 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.62
3
+ Version: 1.63
4
4
  Summary: Generic Tooling
5
5
  Home-page: https://github.com/tylertjburns/cooptools
6
6
  Author: tburns
@@ -673,16 +673,9 @@ class Graph(object):
673
673
  if e_in: msg += f" the end \'{end.name}\' is in the disabled_node_ids"
674
674
  raise Exception(msg)
675
675
 
676
- for x in disabled_node_ids:
677
- self.disable_edges_to_node(
678
- node=self.node_by_name(node_name=x),
679
- disabler=INTERNAL_DISABLER
680
- )
681
-
682
676
  if not ignored_disablers:
683
677
  ignored_disablers = []
684
678
 
685
- """Returns a list of nodes as a path from the given start to the given end in the given graph"""
686
679
  logger.debug(f"Performing A* over map of length: {len(self._graph_dict)}")
687
680
 
688
681
  # Create start and end node
@@ -695,9 +688,11 @@ class Graph(object):
695
688
  open_set = set()
696
689
  closed_set = set()
697
690
 
698
- enabled_connections_to_end = self.edges_to_node(end, only_enabled=True, ignored_disablers=ignored_disablers)
699
- if len(enabled_connections_to_end) != 0:
700
- # 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:
701
696
  open_set.add(start_iter)
702
697
 
703
698
  cc = -1
@@ -742,12 +737,15 @@ class Graph(object):
742
737
  steps,
743
738
  source=start,
744
739
  dest=end,
745
- 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)
746
741
  break
747
742
 
748
743
  # Generate children
749
744
  for new_node in self._graph_dict[current_item.graph_node]: # Adjacent nodes
750
- # 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
+
751
749
  edges = self.edges_between(
752
750
  nodeA=current_item.graph_node,
753
751
  nodeB=new_node
@@ -758,8 +756,8 @@ class Graph(object):
758
756
  if len(enabled_edges) == 0:
759
757
  continue
760
758
 
761
- # find the min-lenght edge
762
- enabled_edges.sort(key=lambda x: x.length)
759
+ # find the min-cost edge
760
+ enabled_edges.sort(key=lambda x: x.cost)
763
761
  min_edge = enabled_edges[0]
764
762
 
765
763
  new_item = AStarMetrics(current_item, new_node, min_edge)
@@ -797,17 +795,9 @@ class Graph(object):
797
795
  failure_reason=failure_reason,
798
796
  )
799
797
  else:
800
- ''' Log Path found '''
801
798
  logger.debug(f"Path found from [{start}] to [{end}] in {len(steps)} steps")
802
799
  ret = results
803
800
 
804
- if disabled_node_ids is not None:
805
- for x in disabled_node_ids:
806
- self.enable_edges_to_node(
807
- node=self.node_by_name(node_name=x),
808
- disabler=INTERNAL_DISABLER
809
- )
810
-
811
801
  return ret
812
802
 
813
803
  def path_length(self, path:List[Node]):
@@ -1041,6 +1031,199 @@ class Graph(object):
1041
1031
  branches_graph_dict.pop(src, None)
1042
1032
  return Graph(branches_graph_dict), steps
1043
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
+ )
1221
+
1222
+ for src in list(source_set):
1223
+ if branches_graph_dict.get(src) == []:
1224
+ branches_graph_dict.pop(src, None)
1225
+ return Graph(branches_graph_dict), steps
1226
+
1044
1227
  @property
1045
1228
  def SectorGraph(self) -> Self:
1046
1229
  client_mappings = self._sec_tree.ClientMappings
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cooptools
3
- Version: 1.62
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.62',
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
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
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