cooptools 1.74__tar.gz → 1.76__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 (191) hide show
  1. {cooptools-1.74 → cooptools-1.76}/PKG-INFO +1 -1
  2. {cooptools-1.74 → cooptools-1.76}/cooptools/common.py +9 -5
  3. {cooptools-1.74 → cooptools-1.76}/cooptools/geometry_utils/vector_utils.py +39 -5
  4. {cooptools-1.74 → cooptools-1.76}/cooptools/graphs/graph.py +13 -3
  5. {cooptools-1.74 → cooptools-1.76}/cooptools/graphs/graph_dcs.py +6 -1
  6. {cooptools-1.74 → cooptools-1.76}/cooptools/sectors/sectorTree/sectorTree.py +8 -4
  7. {cooptools-1.74 → cooptools-1.76}/cooptools.egg-info/PKG-INFO +1 -1
  8. {cooptools-1.74 → cooptools-1.76}/cooptools.egg-info/SOURCES.txt +1 -0
  9. {cooptools-1.74 → cooptools-1.76}/setup.py +1 -1
  10. cooptools-1.76/tests/test_graph_node_classification.py +142 -0
  11. {cooptools-1.74 → cooptools-1.76}/MANIFEST.in +0 -0
  12. {cooptools-1.74 → cooptools-1.76}/README.md +0 -0
  13. {cooptools-1.74 → cooptools-1.76}/cooptools/__init__.py +0 -0
  14. {cooptools-1.74 → cooptools-1.76}/cooptools/anchor.py +0 -0
  15. {cooptools-1.74 → cooptools-1.76}/cooptools/asyncable.py +0 -0
  16. {cooptools-1.74 → cooptools-1.76}/cooptools/cli/CliAtomicUserInteraction.py +0 -0
  17. {cooptools-1.74 → cooptools-1.76}/cooptools/cli/CliMenu.py +0 -0
  18. {cooptools-1.74 → cooptools-1.76}/cooptools/cli/__init__.py +0 -0
  19. {cooptools-1.74 → cooptools-1.76}/cooptools/cli/fileContentReturn.py +0 -0
  20. {cooptools-1.74 → cooptools-1.76}/cooptools/cnxn_info.py +0 -0
  21. {cooptools-1.74 → cooptools-1.76}/cooptools/colors.py +0 -0
  22. {cooptools-1.74 → cooptools-1.76}/cooptools/commandDesignPattern/__init__.py +0 -0
  23. {cooptools-1.74 → cooptools-1.76}/cooptools/commandDesignPattern/commandController.py +0 -0
  24. {cooptools-1.74 → cooptools-1.76}/cooptools/commandDesignPattern/commandProtocol.py +0 -0
  25. {cooptools-1.74 → cooptools-1.76}/cooptools/commandDesignPattern/exceptions.py +0 -0
  26. {cooptools-1.74 → cooptools-1.76}/cooptools/config.py +0 -0
  27. {cooptools-1.74 → cooptools-1.76}/cooptools/coopDataclass.py +0 -0
  28. {cooptools-1.74 → cooptools-1.76}/cooptools/coopEnum.py +0 -0
  29. {cooptools-1.74 → cooptools-1.76}/cooptools/coopthreading.py +0 -0
  30. {cooptools-1.74 → cooptools-1.76}/cooptools/currency.py +0 -0
  31. {cooptools-1.74 → cooptools-1.76}/cooptools/dataRefresher/__init__.py +0 -0
  32. {cooptools-1.74 → cooptools-1.76}/cooptools/dataRefresher/dataHub.py +0 -0
  33. {cooptools-1.74 → cooptools-1.76}/cooptools/dataRefresher/dataRefresher.py +0 -0
  34. {cooptools-1.74 → cooptools-1.76}/cooptools/dataStore/__init__.py +0 -0
  35. {cooptools-1.74 → cooptools-1.76}/cooptools/dataStore/dataProcessor.py +0 -0
  36. {cooptools-1.74 → cooptools-1.76}/cooptools/dataStore/dataStoreProtocol.py +0 -0
  37. {cooptools-1.74 → cooptools-1.76}/cooptools/dataStore/dbConnectionURI.py +0 -0
  38. {cooptools-1.74 → cooptools-1.76}/cooptools/dataStore/inMemoryDataStore.py +0 -0
  39. {cooptools-1.74 → cooptools-1.76}/cooptools/date_utils.py +0 -0
  40. {cooptools-1.74 → cooptools-1.76}/cooptools/decay.py +0 -0
  41. {cooptools-1.74 → cooptools-1.76}/cooptools/decor.py +0 -0
  42. {cooptools-1.74 → cooptools-1.76}/cooptools/dictPolicies.py +0 -0
  43. {cooptools-1.74 → cooptools-1.76}/cooptools/exceptions.py +0 -0
  44. {cooptools-1.74 → cooptools-1.76}/cooptools/expertise/__init__.py +0 -0
  45. {cooptools-1.74 → cooptools-1.76}/cooptools/expertise/expertiseArgs.py +0 -0
  46. {cooptools-1.74 → cooptools-1.76}/cooptools/expertise/expertiseSchedules.py +0 -0
  47. {cooptools-1.74 → cooptools-1.76}/cooptools/finance/__init__.py +0 -0
  48. {cooptools-1.74 → cooptools-1.76}/cooptools/finance/dicounted_cashflow_analysis.py +0 -0
  49. {cooptools-1.74 → cooptools-1.76}/cooptools/finance/fund_projection.py +0 -0
  50. {cooptools-1.74 → cooptools-1.76}/cooptools/finance/fund_projection_renderer.py +0 -0
  51. {cooptools-1.74 → cooptools-1.76}/cooptools/finance/futureValueProjector.py +0 -0
  52. {cooptools-1.74 → cooptools-1.76}/cooptools/finance/growth_projections.py +0 -0
  53. {cooptools-1.74 → cooptools-1.76}/cooptools/finance/reports.py +0 -0
  54. {cooptools-1.74 → cooptools-1.76}/cooptools/finance/utils.py +0 -0
  55. {cooptools-1.74 → cooptools-1.76}/cooptools/geometry_utils/__init__.py +0 -0
  56. {cooptools-1.74 → cooptools-1.76}/cooptools/geometry_utils/circle_utils.py +0 -0
  57. {cooptools-1.74 → cooptools-1.76}/cooptools/geometry_utils/common.py +0 -0
  58. {cooptools-1.74 → cooptools-1.76}/cooptools/geometry_utils/curve_utils.py +0 -0
  59. {cooptools-1.74 → cooptools-1.76}/cooptools/geometry_utils/curves.py +0 -0
  60. {cooptools-1.74 → cooptools-1.76}/cooptools/geometry_utils/line_utils.py +0 -0
  61. {cooptools-1.74 → cooptools-1.76}/cooptools/geometry_utils/polygon_utils.py +0 -0
  62. {cooptools-1.74 → cooptools-1.76}/cooptools/geometry_utils/rect_utils.py +0 -0
  63. {cooptools-1.74 → cooptools-1.76}/cooptools/geometry_utils/triangle_utils.py +0 -0
  64. {cooptools-1.74 → cooptools-1.76}/cooptools/graphs/__init__.py +0 -0
  65. {cooptools-1.74 → cooptools-1.76}/cooptools/graphs/astar_results_viewer.py +0 -0
  66. {cooptools-1.74 → cooptools-1.76}/cooptools/graphs/draw.py +0 -0
  67. {cooptools-1.74 → cooptools-1.76}/cooptools/graphs/flow_field.py +0 -0
  68. {cooptools-1.74 → cooptools-1.76}/cooptools/graphs/graph_definitions.py +0 -0
  69. {cooptools-1.74 → cooptools-1.76}/cooptools/graphs/utils.py +0 -0
  70. {cooptools-1.74 → cooptools-1.76}/cooptools/ideas/__init__.py +0 -0
  71. {cooptools-1.74 → cooptools-1.76}/cooptools/ideas/actionItemTracker/__init__.py +0 -0
  72. {cooptools-1.74 → cooptools-1.76}/cooptools/ideas/actionItemTracker/ai_tracker.py +0 -0
  73. {cooptools-1.74 → cooptools-1.76}/cooptools/ideas/actionItemTracker/dcs.py +0 -0
  74. {cooptools-1.74 → cooptools-1.76}/cooptools/ideas/discreteEventSimulator/__init__.py +0 -0
  75. {cooptools-1.74 → cooptools-1.76}/cooptools/ideas/discreteEventSimulator/discreteEventSimulator.py +0 -0
  76. {cooptools-1.74 → cooptools-1.76}/cooptools/ideas/jsonIO.py +0 -0
  77. {cooptools-1.74 → cooptools-1.76}/cooptools/ideas/metrics.py +0 -0
  78. {cooptools-1.74 → cooptools-1.76}/cooptools/ideas/triggerActionSystem/__init__.py +0 -0
  79. {cooptools-1.74 → cooptools-1.76}/cooptools/ideas/triggerActionSystem/signal.py +0 -0
  80. {cooptools-1.74 → cooptools-1.76}/cooptools/ideas/triggerActionSystem/trigger.py +0 -0
  81. {cooptools-1.74 → cooptools-1.76}/cooptools/loggingHelpers.py +0 -0
  82. {cooptools-1.74 → cooptools-1.76}/cooptools/marchingSquares.py +0 -0
  83. {cooptools-1.74 → cooptools-1.76}/cooptools/materialHandling/__init__.py +0 -0
  84. {cooptools-1.74 → cooptools-1.76}/cooptools/materialHandling/cli.py +0 -0
  85. {cooptools-1.74 → cooptools-1.76}/cooptools/materialHandling/dcs.py +0 -0
  86. {cooptools-1.74 → cooptools-1.76}/cooptools/materialHandling/selectionCriteria.py +0 -0
  87. {cooptools-1.74 → cooptools-1.76}/cooptools/matrixManipulation.py +0 -0
  88. {cooptools-1.74 → cooptools-1.76}/cooptools/os_manip.py +0 -0
  89. {cooptools-1.74 → cooptools-1.76}/cooptools/pandasHelpers.py +0 -0
  90. {cooptools-1.74 → cooptools-1.76}/cooptools/pathing/__init__.py +0 -0
  91. {cooptools-1.74 → cooptools-1.76}/cooptools/pathing/catmullRomFollower.py +0 -0
  92. {cooptools-1.74 → cooptools-1.76}/cooptools/pathing/demo/__init__.py +0 -0
  93. {cooptools-1.74 → cooptools-1.76}/cooptools/pathing/demo/server.py +0 -0
  94. {cooptools-1.74 → cooptools-1.76}/cooptools/pathing/kinematicGoalSeeker.py +0 -0
  95. {cooptools-1.74 → cooptools-1.76}/cooptools/pathing/motion_profile_utils.py +0 -0
  96. {cooptools-1.74 → cooptools-1.76}/cooptools/pathing/orbit_fuzzer.py +0 -0
  97. {cooptools-1.74 → cooptools-1.76}/cooptools/pathing/path_utils.py +0 -0
  98. {cooptools-1.74 → cooptools-1.76}/cooptools/pathing/straightLineFollower.py +0 -0
  99. {cooptools-1.74 → cooptools-1.76}/cooptools/pathing/trajectory_provider.py +0 -0
  100. {cooptools-1.74 → cooptools-1.76}/cooptools/pathing/waypoint.py +0 -0
  101. {cooptools-1.74 → cooptools-1.76}/cooptools/pathing/waypointFollower.py +0 -0
  102. {cooptools-1.74 → cooptools-1.76}/cooptools/physics/__init__.py +0 -0
  103. {cooptools-1.74 → cooptools-1.76}/cooptools/physics/kinematic.py +0 -0
  104. {cooptools-1.74 → cooptools-1.76}/cooptools/physics/kinematic_utils.py +0 -0
  105. {cooptools-1.74 → cooptools-1.76}/cooptools/physics/movement.py +0 -0
  106. {cooptools-1.74 → cooptools-1.76}/cooptools/physics/trajectory.py +0 -0
  107. {cooptools-1.74 → cooptools-1.76}/cooptools/plotting.py +0 -0
  108. {cooptools-1.74 → cooptools-1.76}/cooptools/printing.py +0 -0
  109. {cooptools-1.74 → cooptools-1.76}/cooptools/protocols.py +0 -0
  110. {cooptools-1.74 → cooptools-1.76}/cooptools/qualifiers/__init__.py +0 -0
  111. {cooptools-1.74 → cooptools-1.76}/cooptools/qualifiers/cli.py +0 -0
  112. {cooptools-1.74 → cooptools-1.76}/cooptools/qualifiers/qualifier.py +0 -0
  113. {cooptools-1.74 → cooptools-1.76}/cooptools/randoms.py +0 -0
  114. {cooptools-1.74 → cooptools-1.76}/cooptools/register.py +0 -0
  115. {cooptools-1.74 → cooptools-1.76}/cooptools/reservation/__init__.py +0 -0
  116. {cooptools-1.74 → cooptools-1.76}/cooptools/reservation/dcs.py +0 -0
  117. {cooptools-1.74 → cooptools-1.76}/cooptools/reservation/enums.py +0 -0
  118. {cooptools-1.74 → cooptools-1.76}/cooptools/reservation/reservationmanager.py +0 -0
  119. {cooptools-1.74 → cooptools-1.76}/cooptools/retry.py +0 -0
  120. {cooptools-1.74 → cooptools-1.76}/cooptools/sectors/__init__.py +0 -0
  121. {cooptools-1.74 → cooptools-1.76}/cooptools/sectors/grids/__init__.py +0 -0
  122. {cooptools-1.74 → cooptools-1.76}/cooptools/sectors/grids/gridState.py +0 -0
  123. {cooptools-1.74 → cooptools-1.76}/cooptools/sectors/grids/grid_base.py +0 -0
  124. {cooptools-1.74 → cooptools-1.76}/cooptools/sectors/grids/hexGrid.py +0 -0
  125. {cooptools-1.74 → cooptools-1.76}/cooptools/sectors/grids/rectGrid.py +0 -0
  126. {cooptools-1.74 → cooptools-1.76}/cooptools/sectors/hex_utils.py +0 -0
  127. {cooptools-1.74 → cooptools-1.76}/cooptools/sectors/sect_utils.py +0 -0
  128. {cooptools-1.74 → cooptools-1.76}/cooptools/sectors/sectorTree/__init__.py +0 -0
  129. {cooptools-1.74 → cooptools-1.76}/cooptools/statistics/__init__.py +0 -0
  130. {cooptools-1.74 → cooptools-1.76}/cooptools/statistics/activityReport.py +0 -0
  131. {cooptools-1.74 → cooptools-1.76}/cooptools/statistics/controlChart/__init__.py +0 -0
  132. {cooptools-1.74 → cooptools-1.76}/cooptools/statistics/controlChart/controlChart.py +0 -0
  133. {cooptools-1.74 → cooptools-1.76}/cooptools/statistics/controlChart/plotting.py +0 -0
  134. {cooptools-1.74 → cooptools-1.76}/cooptools/statistics/oee/__init__.py +0 -0
  135. {cooptools-1.74 → cooptools-1.76}/cooptools/statistics/oee/oeeHub.py +0 -0
  136. {cooptools-1.74 → cooptools-1.76}/cooptools/taskProcessing/__init__.py +0 -0
  137. {cooptools-1.74 → cooptools-1.76}/cooptools/taskProcessing/dcs.py +0 -0
  138. {cooptools-1.74 → cooptools-1.76}/cooptools/taskProcessing/taskProcessor.py +0 -0
  139. {cooptools-1.74 → cooptools-1.76}/cooptools/timeTracker/__init__.py +0 -0
  140. {cooptools-1.74 → cooptools-1.76}/cooptools/timeTracker/decay.py +0 -0
  141. {cooptools-1.74 → cooptools-1.76}/cooptools/timeTracker/timeTracker.py +0 -0
  142. {cooptools-1.74 → cooptools-1.76}/cooptools/timeWindow.py +0 -0
  143. {cooptools-1.74 → cooptools-1.76}/cooptools/toggles.py +0 -0
  144. {cooptools-1.74 → cooptools-1.76}/cooptools/transform.py +0 -0
  145. {cooptools-1.74 → cooptools-1.76}/cooptools/trends.py +0 -0
  146. {cooptools-1.74 → cooptools-1.76}/cooptools/typeProviders.py +0 -0
  147. {cooptools-1.74 → cooptools-1.76}/cooptools/typevalidation.py +0 -0
  148. {cooptools-1.74 → cooptools-1.76}/cooptools/version.py +0 -0
  149. {cooptools-1.74 → cooptools-1.76}/cooptools.egg-info/dependency_links.txt +0 -0
  150. {cooptools-1.74 → cooptools-1.76}/cooptools.egg-info/not-zip-safe +0 -0
  151. {cooptools-1.74 → cooptools-1.76}/cooptools.egg-info/requires.txt +0 -0
  152. {cooptools-1.74 → cooptools-1.76}/cooptools.egg-info/top_level.txt +0 -0
  153. {cooptools-1.74 → cooptools-1.76}/requirements.txt +0 -0
  154. {cooptools-1.74 → cooptools-1.76}/setup.cfg +0 -0
  155. {cooptools-1.74 → cooptools-1.76}/tests/test_CliAtomicUserInteraction.py +0 -0
  156. {cooptools-1.74 → cooptools-1.76}/tests/test_asyncable.py +0 -0
  157. {cooptools-1.74 → cooptools-1.76}/tests/test_catmullRomFollower.py +0 -0
  158. {cooptools-1.74 → cooptools-1.76}/tests/test_cdp_bugs.py +0 -0
  159. {cooptools-1.74 → cooptools-1.76}/tests/test_commandDesignPattern.py +0 -0
  160. {cooptools-1.74 → cooptools-1.76}/tests/test_common.py +0 -0
  161. {cooptools-1.74 → cooptools-1.76}/tests/test_config.py +0 -0
  162. {cooptools-1.74 → cooptools-1.76}/tests/test_coopEnum.py +0 -0
  163. {cooptools-1.74 → cooptools-1.76}/tests/test_currency.py +0 -0
  164. {cooptools-1.74 → cooptools-1.76}/tests/test_curves.py +0 -0
  165. {cooptools-1.74 → cooptools-1.76}/tests/test_date_utils.py +0 -0
  166. {cooptools-1.74 → cooptools-1.76}/tests/test_flow_field.py +0 -0
  167. {cooptools-1.74 → cooptools-1.76}/tests/test_flow_field_perf.py +0 -0
  168. {cooptools-1.74 → cooptools-1.76}/tests/test_fund_projection.py +0 -0
  169. {cooptools-1.74 → cooptools-1.76}/tests/test_kinematicGoalSeeker.py +0 -0
  170. {cooptools-1.74 → cooptools-1.76}/tests/test_kinematic_utils.py +0 -0
  171. {cooptools-1.74 → cooptools-1.76}/tests/test_os_manip.py +0 -0
  172. {cooptools-1.74 → cooptools-1.76}/tests/test_pandas.py +0 -0
  173. {cooptools-1.74 → cooptools-1.76}/tests/test_path_utils.py +0 -0
  174. {cooptools-1.74 → cooptools-1.76}/tests/test_printing.py +0 -0
  175. {cooptools-1.74 → cooptools-1.76}/tests/test_qualifiers.py +0 -0
  176. {cooptools-1.74 → cooptools-1.76}/tests/test_sectors.py +0 -0
  177. {cooptools-1.74 → cooptools-1.76}/tests/test_sectors_hex.py +0 -0
  178. {cooptools-1.74 → cooptools-1.76}/tests/test_sectors_sectorTree.py +0 -0
  179. {cooptools-1.74 → cooptools-1.76}/tests/test_statistics.py +0 -0
  180. {cooptools-1.74 → cooptools-1.76}/tests/test_straightLineFollower.py +0 -0
  181. {cooptools-1.74 → cooptools-1.76}/tests/test_target_seek.py +0 -0
  182. {cooptools-1.74 → cooptools-1.76}/tests/test_tasks.py +0 -0
  183. {cooptools-1.74 → cooptools-1.76}/tests/test_toggles.py +0 -0
  184. {cooptools-1.74 → cooptools-1.76}/tests/test_trajectory.py +0 -0
  185. {cooptools-1.74 → cooptools-1.76}/tests/test_trends.py +0 -0
  186. {cooptools-1.74 → cooptools-1.76}/tests/test_typevalidation.py +0 -0
  187. {cooptools-1.74 → cooptools-1.76}/tests/test_vector_utils.py +0 -0
  188. {cooptools-1.74 → cooptools-1.76}/tests/test_version.py +0 -0
  189. {cooptools-1.74 → cooptools-1.76}/tests/test_waypointFollower.py +0 -0
  190. {cooptools-1.74 → cooptools-1.76}/tests/tests_graph.py +0 -0
  191. {cooptools-1.74 → cooptools-1.76}/tests/tests_gridsystem.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cooptools
3
- Version: 1.74
3
+ Version: 1.76
4
4
  Summary: Generic Tooling
5
5
  Home-page: https://github.com/tylertjburns/cooptools
6
6
  Author: tburns
@@ -1,6 +1,6 @@
1
1
  import math
2
2
  import uuid
3
- from typing import List, Union, Tuple, Iterable, Callable, Sequence, TypeVar, Dict
3
+ from typing import List, Union, Tuple, Iterable, Callable, Sequence, TypeVar, Dict, Optional
4
4
  import itertools
5
5
  import numpy as np
6
6
  import bisect
@@ -145,11 +145,15 @@ def verify_unique(lst: Iterable, error_msg: str = None):
145
145
  def verify_len_match(iterable1,
146
146
  iterable2,
147
147
  error_msg: str = None,
148
- block:bool = True):
149
- msg = f"{iterable1} and {iterable2} do not have the same length ({len(iterable1)} vs {len(iterable2)})"
148
+ block:bool = True) -> Optional[int]:
149
+ # Length is compared before the message is built. This sits under
150
+ # rect_contains_point on the sector-tree insert path, where formatting a
151
+ # message for a check that passes costs more than the check itself.
152
+ if (l1:=len(iterable1)) == len(iterable2):
153
+ return l1
150
154
 
151
- tst = lambda: len(iterable1) == len(iterable2)
152
- verify(tst, msg, error_msg, block=block)
155
+ msg = f"{iterable1} and {iterable2} do not have the same length ({len(iterable1)} vs {len(iterable2)})"
156
+ verify(lambda: False, msg, error_msg, block=block)
153
157
 
154
158
  def verify_len(iterable, length: int, error_msg: str = None):
155
159
  msg = f"{iterable} does not have len {length} ({len(iterable)})"
@@ -108,12 +108,30 @@ def vector_between(start: FloatVec,
108
108
  allow_diff_lengths: bool = False
109
109
  ) -> FloatVec:
110
110
  if not allow_diff_lengths:
111
- verify_len_match(start, end)
111
+ l_start = verify_len_match(start, end)
112
+ l_end = l_start
113
+ else:
114
+ l_start, l_end = len(start), len(end)
115
+
116
+ # 2- and 3-D vectors are most of the traffic here, and for them the loop
117
+ # below costs far more than the subtractions it performs: a range, two
118
+ # bounds-checked conditionals per axis, a list append per axis, and a
119
+ # tuple() at the end. Naming the axes skips all of it.
120
+ #
121
+ # Equal lengths are part of the condition because that is the only case in
122
+ # which the loop's padding cannot apply; an unequal pair falls through and
123
+ # is padded or truncated exactly as before.
124
+ if l_start == l_end == 2:
125
+ return (end[0] - start[0], end[1] - start[1])
126
+
127
+ if l_start == l_end == 3:
128
+ return (end[0] - start[0], end[1] - start[1], end[2] - start[2])
112
129
 
113
130
  ret = []
114
- for idx in range(len(start)):
115
- e = end[idx] if idx < len(end) else 0
116
- s = start[idx] if idx < len(start) else 0
131
+
132
+ for idx in range(l_start):
133
+ e = end[idx] if idx < l_end else 0
134
+ s = start[idx] if idx < l_start else 0
117
135
 
118
136
  ret.append((e - s))
119
137
 
@@ -175,7 +193,23 @@ def scale_vector_length(a: FloatVec, scale: float) -> FloatVec:
175
193
 
176
194
 
177
195
  def vector_len(a: FloatVec, squared: bool = False) -> float:
178
- sum_sq = sum(ii ** 2 for ii in a)
196
+ # 2- and 3-D vectors are most of the traffic here, and for them the general
197
+ # branch spends more on interpreter overhead than on arithmetic: a generator
198
+ # object plus one resumption of it per axis, driven by sum(). Naming the
199
+ # axes skips both.
200
+ #
201
+ # sum() applies compensated (Neumaier) summation to floats, so for three
202
+ # terms it can land a ULP away from the plain addition used here. That
203
+ # difference is accepted: a ULP sits far below the precision a caller of a
204
+ # vector length can meaningfully rely on.
205
+ len_a = len(a)
206
+ if len_a == 2:
207
+ sum_sq = a[0] * a[0] + a[1] * a[1]
208
+ elif len_a == 3:
209
+ sum_sq = a[0] * a[0] + a[1] * a[1] + a[2] * a[2]
210
+ else:
211
+ sum_sq = sum(ii * ii for ii in a)
212
+
179
213
  return sum_sq if squared else math.sqrt(sum_sq)
180
214
 
181
215
 
@@ -306,12 +306,17 @@ class Graph(object):
306
306
 
307
307
  @property
308
308
  def Sources(self) -> List[Node]:
309
- dests = self.DestinationNodes
309
+ # Membership is tested once per node, so the destinations are hashed
310
+ # rather than scanned -- DestinationNodes hands back a list.
311
+ dests = set(self.DestinationNodes)
310
312
  return [n for n, cnxn in self._graph_dict.items() if n not in dests and len(cnxn) != 0]
311
313
 
312
314
  @property
313
315
  def Sinks(self) -> List[Node]:
314
- return [n for n, cnxn in self._graph_dict.items() if len(cnxn) == 0 and n not in self.Orphans]
316
+ # Orphans walks the whole node-edge map, so it is built once for the
317
+ # scan instead of once per node.
318
+ orphans = set(self.Orphans)
319
+ return [n for n, cnxn in self._graph_dict.items() if len(cnxn) == 0 and n not in orphans]
315
320
 
316
321
  @property
317
322
  def Orphans(self) -> List[Node]:
@@ -734,6 +739,11 @@ class Graph(object):
734
739
  if not ignored_disablers:
735
740
  ignored_disablers = []
736
741
 
742
+ # Hoisted out of the expansion loop below, which tests it once per edge
743
+ # considered -- millions of times on a large graph. Edge.enabled needs a
744
+ # set, so building one here keeps that per-edge cost to a lookup.
745
+ _ignored_disablers_set = set(ignored_disablers)
746
+
737
747
  logger.debug(f"Performing A* over map of length: {len(self._graph_dict)}")
738
748
 
739
749
  # Create start and end node
@@ -816,7 +826,7 @@ class Graph(object):
816
826
  )
817
827
 
818
828
  # Dont evaluate this node if the edge is not enabled
819
- enabled_edges = [x for x in edges if x.enabled(ignored_disablers=set(ignored_disablers))]
829
+ enabled_edges = [x for x in edges if x.enabled(ignored_disablers=_ignored_disablers_set)]
820
830
  if len(enabled_edges) == 0:
821
831
  continue
822
832
 
@@ -165,8 +165,13 @@ class Edge(object):
165
165
  return self.length
166
166
 
167
167
  def enabled(self, ignored_disablers: set = None):
168
+ # An edge with no disablers is enabled whatever the caller ignores, and
169
+ # that is the overwhelmingly common case on a pathfinding hot loop --
170
+ # answering it first avoids allocating an empty set per call.
171
+ if not self._disablers:
172
+ return True
168
173
  if ignored_disablers is None:
169
- ignored_disablers = set()
174
+ return False
170
175
  return self._disablers.issubset(ignored_disablers)
171
176
 
172
177
 
@@ -161,10 +161,14 @@ class SectorTree:
161
161
  for client, type in clients.items():
162
162
  ii += 1
163
163
  if self.lvl == 0:
164
- if client in self.Clients:
165
- logger.info(f"[{self._id}] User requests updating {ii}/{len(clients)} [{client}]")
166
- else:
167
- logger.info(f"[{self._id}] User requests adding {ii}/{len(clients)} [{client}]")
164
+ # Whether this client is already known is read from
165
+ # _last_mapped_pos -- the same dict remove_clients treats as the
166
+ # membership record -- because it answers in constant time. The
167
+ # Clients property flattens every sector mapping in the tree, so
168
+ # consulting it once per client made registering a batch
169
+ # quadratic in the size of the batch.
170
+ verb = "updating" if client in self._last_mapped_pos else "adding"
171
+ logger.info(f"[{self._id}] User requests {verb} {ii}/{len(clients)} [{client}]")
168
172
 
169
173
  if not client.__hash__:
170
174
  raise Exception(f"Client {client} must be hashable, but type {type(client)} is not")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cooptools
3
- Version: 1.74
3
+ Version: 1.76
4
4
  Summary: Generic Tooling
5
5
  Home-page: https://github.com/tylertjburns/cooptools
6
6
  Author: tburns
@@ -163,6 +163,7 @@ tests/test_date_utils.py
163
163
  tests/test_flow_field.py
164
164
  tests/test_flow_field_perf.py
165
165
  tests/test_fund_projection.py
166
+ tests/test_graph_node_classification.py
166
167
  tests/test_kinematicGoalSeeker.py
167
168
  tests/test_kinematic_utils.py
168
169
  tests/test_os_manip.py
@@ -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.74',
10
+ version='1.76',
11
11
  description='Generic Tooling',
12
12
  url='https://github.com/tylertjburns/cooptools',
13
13
  author='tburns',
@@ -0,0 +1,142 @@
1
+ import unittest
2
+
3
+ from cooptools.common import verify_len_match
4
+ from cooptools.graphs import Graph, Node
5
+ from cooptools.graphs.graph_dcs import Edge
6
+
7
+
8
+ class TestNodeClassification(unittest.TestCase):
9
+ """Sources/Sinks/Orphans partition a graph's nodes by their edge directions."""
10
+
11
+ @staticmethod
12
+ def _graph():
13
+ # a -> b -> c (a is a source, c is a sink)
14
+ # d (no edges at all -- an orphan)
15
+ a = Node(name='A', pos=(0.0, 0.0))
16
+ b = Node(name='B', pos=(1.0, 0.0))
17
+ c = Node(name='C', pos=(2.0, 0.0))
18
+ d = Node(name='D', pos=(9.0, 9.0))
19
+ return Graph({a: [b], b: [c], c: [], d: []})
20
+
21
+ def test_Sources_returns_only_nodes_nothing_points_at(self):
22
+ self.assertEqual(['A'], [n.name for n in self._graph().Sources])
23
+
24
+ def test_Sinks_returns_connected_nodes_with_no_outbound_edge(self):
25
+ self.assertEqual(['C'], [n.name for n in self._graph().Sinks])
26
+
27
+ def test_Sinks_excludes_orphans(self):
28
+ # D has no edges in either direction, so it is an orphan rather than a
29
+ # sink -- the two categories must not both claim it.
30
+ graph = self._graph()
31
+ self.assertNotIn('D', [n.name for n in graph.Sinks])
32
+ self.assertIn('D', [n.name for n in graph.Orphans])
33
+
34
+ def test_Orphans_returns_only_wholly_unconnected_nodes(self):
35
+ self.assertEqual(['D'], [n.name for n in self._graph().Orphans])
36
+
37
+ def test_a_node_is_never_both_a_source_and_a_sink(self):
38
+ graph = self._graph()
39
+ sources = {n.name for n in graph.Sources}
40
+ sinks = {n.name for n in graph.Sinks}
41
+ self.assertEqual(set(), sources & sinks)
42
+
43
+ def test_fully_connected_cycle_has_no_sources_sinks_or_orphans(self):
44
+ a = Node(name='A', pos=(0.0, 0.0))
45
+ b = Node(name='B', pos=(1.0, 0.0))
46
+ graph = Graph({a: [b], b: [a]})
47
+ self.assertEqual([], graph.Sources)
48
+ self.assertEqual([], graph.Sinks)
49
+ self.assertEqual([], graph.Orphans)
50
+
51
+
52
+ class TestEdgeEnabled(unittest.TestCase):
53
+ """An edge is enabled when every disabler on it is one the caller ignores."""
54
+
55
+ @staticmethod
56
+ def _edge(disablers=None):
57
+ return Edge(Node('A', (0.0, 0.0)), Node('B', (1.0, 1.0)), disablers=disablers)
58
+
59
+ def test_edge_without_disablers_is_enabled_when_nothing_is_ignored(self):
60
+ self.assertTrue(self._edge().enabled(None))
61
+
62
+ def test_edge_without_disablers_is_enabled_whatever_is_ignored(self):
63
+ self.assertTrue(self._edge().enabled({'anything'}))
64
+
65
+ def test_disabled_edge_is_not_enabled_when_nothing_is_ignored(self):
66
+ self.assertFalse(self._edge(['d1']).enabled(None))
67
+ self.assertFalse(self._edge(['d1']).enabled(set()))
68
+
69
+ def test_disabled_edge_is_enabled_once_its_disabler_is_ignored(self):
70
+ self.assertTrue(self._edge(['d1']).enabled({'d1'}))
71
+
72
+ def test_edge_needs_every_one_of_its_disablers_ignored(self):
73
+ edge = self._edge(['d1', 'd2'])
74
+ self.assertFalse(edge.enabled({'d1'}))
75
+ self.assertTrue(edge.enabled({'d1', 'd2'}))
76
+
77
+ def test_ignoring_an_unrelated_disabler_does_not_enable_the_edge(self):
78
+ self.assertFalse(self._edge(['d1']).enabled({'other'}))
79
+
80
+ def test_ignored_disablers_may_be_any_iterable(self):
81
+ self.assertTrue(self._edge(['d1']).enabled(['d1']))
82
+
83
+
84
+ class TestAStarIgnoredDisablers(unittest.TestCase):
85
+ """A* may only traverse a disabled edge when told to ignore its disabler."""
86
+
87
+ @staticmethod
88
+ def _graph_with_a_disabled_shortcut():
89
+ # A -> B is the only route; disabling it makes the graph unroutable.
90
+ a = Node(name='A', pos=(0.0, 0.0))
91
+ b = Node(name='B', pos=(1.0, 0.0))
92
+ graph = Graph({a: [b], b: []})
93
+ for edge in graph.Edges.values():
94
+ edge.add_disabler('BLOCKED')
95
+ return graph, a, b
96
+
97
+ def test_route_is_not_found_across_a_disabled_edge(self):
98
+ graph, a, b = self._graph_with_a_disabled_shortcut()
99
+ self.assertFalse(graph.astar(a, b).Success)
100
+
101
+ def test_route_is_found_when_the_disabler_is_ignored(self):
102
+ graph, a, b = self._graph_with_a_disabled_shortcut()
103
+ results = graph.astar(a, b, ignored_disablers=frozenset({'BLOCKED'}))
104
+ self.assertTrue(results.Success)
105
+ self.assertEqual(['A', 'B'], [n.name for n in results.nodes])
106
+
107
+ def test_ignored_disablers_accepts_a_list_as_well_as_a_set(self):
108
+ graph, a, b = self._graph_with_a_disabled_shortcut()
109
+ self.assertTrue(graph.astar(a, b, ignored_disablers=['BLOCKED']).Success)
110
+
111
+ def test_ignoring_an_unrelated_disabler_leaves_the_edge_closed(self):
112
+ graph, a, b = self._graph_with_a_disabled_shortcut()
113
+ self.assertFalse(graph.astar(a, b, ignored_disablers=frozenset({'SOMETHING_ELSE'})).Success)
114
+
115
+
116
+ class TestVerifyLenMatch(unittest.TestCase):
117
+
118
+ def test_matching_lengths_return_the_shared_length(self):
119
+ # The length is handed back so callers that need it -- vector_between
120
+ # being the reason -- do not have to measure the operands again.
121
+ self.assertEqual(2, verify_len_match((1, 2), (3, 4)))
122
+
123
+ def test_two_empties_match_and_report_zero(self):
124
+ self.assertEqual(0, verify_len_match((), ()))
125
+
126
+ def test_mismatch_returns_nothing_when_not_blocking(self):
127
+ self.assertIsNone(verify_len_match((1, 2, 3), (4, 5), block=False))
128
+
129
+ def test_mismatched_lengths_raise(self):
130
+ with self.assertRaises(ValueError):
131
+ verify_len_match((1, 2, 3), (4, 5))
132
+
133
+ def test_the_error_reports_both_lengths(self):
134
+ with self.assertRaises(ValueError) as ctx:
135
+ verify_len_match((1, 2, 3), (4, 5))
136
+ self.assertIn('3', str(ctx.exception))
137
+ self.assertIn('2', str(ctx.exception))
138
+
139
+
140
+
141
+ if __name__ == '__main__':
142
+ unittest.main()
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