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.
- {cooptools-1.61 → cooptools-1.63}/PKG-INFO +1 -1
- {cooptools-1.61 → cooptools-1.63}/cooptools/graphs/graph.py +339 -67
- {cooptools-1.61 → cooptools-1.63}/cooptools.egg-info/PKG-INFO +1 -1
- {cooptools-1.61 → cooptools-1.63}/setup.py +1 -1
- {cooptools-1.61 → cooptools-1.63}/MANIFEST.in +0 -0
- {cooptools-1.61 → cooptools-1.63}/README.md +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/__init__.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/anchor.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/asyncable.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/cli/CliAtomicUserInteraction.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/cli/CliMenu.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/cli/__init__.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/cli/fileContentReturn.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/cnxn_info.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/colors.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/commandDesignPattern/__init__.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/commandDesignPattern/commandController.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/commandDesignPattern/commandProtocol.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/commandDesignPattern/exceptions.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/common.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/config.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/coopDataclass.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/coopEnum.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/coopthreading.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/currency.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/dataRefresher/__init__.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/dataRefresher/dataHub.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/dataRefresher/dataRefresher.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/dataStore/__init__.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/dataStore/dataProcessor.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/dataStore/dataStoreProtocol.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/dataStore/dbConnectionURI.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/dataStore/inMemoryDataStore.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/date_utils.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/decay.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/decor.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/dictPolicies.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/exceptions.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/expertise/__init__.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/expertise/expertiseArgs.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/expertise/expertiseSchedules.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/finance/__init__.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/finance/dicounted_cashflow_analysis.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/finance/fund_projection.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/finance/fund_projection_renderer.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/finance/futureValueProjector.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/finance/growth_projections.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/finance/reports.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/finance/utils.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/geometry_utils/__init__.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/geometry_utils/circle_utils.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/geometry_utils/common.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/geometry_utils/curve_utils.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/geometry_utils/line_utils.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/geometry_utils/polygon_utils.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/geometry_utils/rect_utils.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/geometry_utils/triangle_utils.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/geometry_utils/vector_utils.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/graphs/__init__.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/graphs/astar_results_viewer.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/graphs/draw.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/graphs/graph_dcs.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/graphs/graph_definitions.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/graphs/utils.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/ideas/__init__.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/ideas/actionItemTracker/__init__.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/ideas/actionItemTracker/ai_tracker.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/ideas/actionItemTracker/dcs.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/ideas/discreteEventSimulator/__init__.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/ideas/discreteEventSimulator/discreteEventSimulator.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/ideas/jsonIO.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/ideas/metrics.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/ideas/triggerActionSystem/__init__.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/ideas/triggerActionSystem/signal.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/ideas/triggerActionSystem/trigger.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/loggingHelpers.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/marchingSquares.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/materialHandling/__init__.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/materialHandling/cli.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/materialHandling/dcs.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/materialHandling/selectionCriteria.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/matrixManipulation.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/os_manip.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/pandasHelpers.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/physics/__init__.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/physics/kinematic.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/physics/kinematic_utils.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/physics/movement.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/physics/trajectory.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/physics/waypoint.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/physics/waypointFollower.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/plotting.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/printing.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/protocols.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/qualifiers/__init__.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/qualifiers/cli.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/qualifiers/qualifier.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/randoms.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/register.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/reservation/__init__.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/reservation/dcs.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/reservation/enums.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/reservation/reservationmanager.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/retry.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/sectors/__init__.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/sectors/grids/__init__.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/sectors/grids/gridState.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/sectors/grids/grid_base.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/sectors/grids/hexGrid.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/sectors/grids/rectGrid.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/sectors/hex_utils.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/sectors/sect_utils.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/sectors/sectorTree/__init__.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/sectors/sectorTree/sectorTree.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/statistics/__init__.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/statistics/activityReport.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/statistics/controlChart/__init__.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/statistics/controlChart/controlChart.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/statistics/controlChart/plotting.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/statistics/oee/__init__.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/statistics/oee/oeeHub.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/taskProcessing/__init__.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/taskProcessing/dcs.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/taskProcessing/taskProcessor.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/timeTracker/__init__.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/timeTracker/decay.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/timeTracker/timeTracker.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/timeWindow.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/toggles.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/transform.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/trends.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/typeProviders.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/typevalidation.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools/version.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools.egg-info/SOURCES.txt +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools.egg-info/dependency_links.txt +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools.egg-info/not-zip-safe +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools.egg-info/requires.txt +0 -0
- {cooptools-1.61 → cooptools-1.63}/cooptools.egg-info/top_level.txt +0 -0
- {cooptools-1.61 → cooptools-1.63}/requirements.txt +0 -0
- {cooptools-1.61 → cooptools-1.63}/setup.cfg +0 -0
- {cooptools-1.61 → cooptools-1.63}/tests/test_CliAtomicUserInteraction.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/tests/test_asyncable.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/tests/test_cdp_bugs.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/tests/test_commandDesignPattern.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/tests/test_common.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/tests/test_config.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/tests/test_coopEnum.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/tests/test_currency.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/tests/test_date_utils.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/tests/test_fund_projection.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/tests/test_os_manip.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/tests/test_pandas.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/tests/test_printing.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/tests/test_qualifiers.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/tests/test_sectors.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/tests/test_sectors_hex.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/tests/test_sectors_sectorTree.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/tests/test_statistics.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/tests/test_tasks.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/tests/test_toggles.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/tests/test_trends.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/tests/test_typevalidation.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/tests/test_vector_utils.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/tests/test_version.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/tests/tests_graph.py +0 -0
- {cooptools-1.61 → cooptools-1.63}/tests/tests_gridsystem.py +0 -0
|
@@ -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[
|
|
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[
|
|
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[
|
|
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[
|
|
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[
|
|
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
|
|
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
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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)
|
|
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
|
-
#
|
|
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-
|
|
753
|
-
enabled_edges.sort(key=lambda x: x.
|
|
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
|
-
|
|
777
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
|
927
|
-
current_targets.remove(target)
|
|
995
|
+
if target not in current_targets:
|
|
928
996
|
continue
|
|
929
997
|
|
|
930
|
-
|
|
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
|
-
|
|
936
|
-
|
|
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
|
-
|
|
939
|
-
|
|
940
|
-
|
|
1016
|
+
if first_visit in current_targets:
|
|
1017
|
+
current_targets.discard(first_visit)
|
|
1018
|
+
reached_targets.add(first_visit)
|
|
941
1019
|
|
|
942
|
-
|
|
943
|
-
active_nodes.add(first_visit)
|
|
1020
|
+
_snap("Edges added", new_edges=new_edges, best_routes=best_routes_display)
|
|
944
1021
|
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
File without changes
|