cooptools 1.63__tar.gz → 1.65__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 (176) hide show
  1. {cooptools-1.63 → cooptools-1.65}/PKG-INFO +1 -1
  2. {cooptools-1.63 → cooptools-1.65}/cooptools/decor.py +39 -7
  3. {cooptools-1.63 → cooptools-1.65}/cooptools/geometry_utils/circle_utils.py +158 -2
  4. {cooptools-1.63 → cooptools-1.65}/cooptools/graphs/graph.py +31 -16
  5. cooptools-1.65/cooptools/physics/demo/server.py +136 -0
  6. {cooptools-1.63 → cooptools-1.65}/cooptools/physics/kinematic.py +88 -10
  7. cooptools-1.65/cooptools/physics/kinematic_utils.py +331 -0
  8. cooptools-1.65/cooptools/physics/trajectory.py +718 -0
  9. cooptools-1.65/cooptools/physics/waypointFollower.py +320 -0
  10. cooptools-1.65/cooptools/statistics/oee/__init__.py +0 -0
  11. {cooptools-1.63 → cooptools-1.65}/cooptools.egg-info/PKG-INFO +1 -1
  12. {cooptools-1.63 → cooptools-1.65}/cooptools.egg-info/SOURCES.txt +6 -0
  13. {cooptools-1.63 → cooptools-1.65}/setup.py +1 -1
  14. cooptools-1.65/tests/test_kinematic.py +174 -0
  15. cooptools-1.65/tests/test_kinematic_utils.py +310 -0
  16. cooptools-1.65/tests/test_trajectory.py +562 -0
  17. cooptools-1.65/tests/test_waypointFollower.py +470 -0
  18. cooptools-1.63/cooptools/physics/kinematic_utils.py +0 -382
  19. cooptools-1.63/cooptools/physics/trajectory.py +0 -343
  20. cooptools-1.63/cooptools/physics/waypointFollower.py +0 -206
  21. {cooptools-1.63 → cooptools-1.65}/MANIFEST.in +0 -0
  22. {cooptools-1.63 → cooptools-1.65}/README.md +0 -0
  23. {cooptools-1.63 → cooptools-1.65}/cooptools/__init__.py +0 -0
  24. {cooptools-1.63 → cooptools-1.65}/cooptools/anchor.py +0 -0
  25. {cooptools-1.63 → cooptools-1.65}/cooptools/asyncable.py +0 -0
  26. {cooptools-1.63 → cooptools-1.65}/cooptools/cli/CliAtomicUserInteraction.py +0 -0
  27. {cooptools-1.63 → cooptools-1.65}/cooptools/cli/CliMenu.py +0 -0
  28. {cooptools-1.63 → cooptools-1.65}/cooptools/cli/__init__.py +0 -0
  29. {cooptools-1.63 → cooptools-1.65}/cooptools/cli/fileContentReturn.py +0 -0
  30. {cooptools-1.63 → cooptools-1.65}/cooptools/cnxn_info.py +0 -0
  31. {cooptools-1.63 → cooptools-1.65}/cooptools/colors.py +0 -0
  32. {cooptools-1.63 → cooptools-1.65}/cooptools/commandDesignPattern/__init__.py +0 -0
  33. {cooptools-1.63 → cooptools-1.65}/cooptools/commandDesignPattern/commandController.py +0 -0
  34. {cooptools-1.63 → cooptools-1.65}/cooptools/commandDesignPattern/commandProtocol.py +0 -0
  35. {cooptools-1.63 → cooptools-1.65}/cooptools/commandDesignPattern/exceptions.py +0 -0
  36. {cooptools-1.63 → cooptools-1.65}/cooptools/common.py +0 -0
  37. {cooptools-1.63 → cooptools-1.65}/cooptools/config.py +0 -0
  38. {cooptools-1.63 → cooptools-1.65}/cooptools/coopDataclass.py +0 -0
  39. {cooptools-1.63 → cooptools-1.65}/cooptools/coopEnum.py +0 -0
  40. {cooptools-1.63 → cooptools-1.65}/cooptools/coopthreading.py +0 -0
  41. {cooptools-1.63 → cooptools-1.65}/cooptools/currency.py +0 -0
  42. {cooptools-1.63 → cooptools-1.65}/cooptools/dataRefresher/__init__.py +0 -0
  43. {cooptools-1.63 → cooptools-1.65}/cooptools/dataRefresher/dataHub.py +0 -0
  44. {cooptools-1.63 → cooptools-1.65}/cooptools/dataRefresher/dataRefresher.py +0 -0
  45. {cooptools-1.63 → cooptools-1.65}/cooptools/dataStore/__init__.py +0 -0
  46. {cooptools-1.63 → cooptools-1.65}/cooptools/dataStore/dataProcessor.py +0 -0
  47. {cooptools-1.63 → cooptools-1.65}/cooptools/dataStore/dataStoreProtocol.py +0 -0
  48. {cooptools-1.63 → cooptools-1.65}/cooptools/dataStore/dbConnectionURI.py +0 -0
  49. {cooptools-1.63 → cooptools-1.65}/cooptools/dataStore/inMemoryDataStore.py +0 -0
  50. {cooptools-1.63 → cooptools-1.65}/cooptools/date_utils.py +0 -0
  51. {cooptools-1.63 → cooptools-1.65}/cooptools/decay.py +0 -0
  52. {cooptools-1.63 → cooptools-1.65}/cooptools/dictPolicies.py +0 -0
  53. {cooptools-1.63 → cooptools-1.65}/cooptools/exceptions.py +0 -0
  54. {cooptools-1.63 → cooptools-1.65}/cooptools/expertise/__init__.py +0 -0
  55. {cooptools-1.63 → cooptools-1.65}/cooptools/expertise/expertiseArgs.py +0 -0
  56. {cooptools-1.63 → cooptools-1.65}/cooptools/expertise/expertiseSchedules.py +0 -0
  57. {cooptools-1.63 → cooptools-1.65}/cooptools/finance/__init__.py +0 -0
  58. {cooptools-1.63 → cooptools-1.65}/cooptools/finance/dicounted_cashflow_analysis.py +0 -0
  59. {cooptools-1.63 → cooptools-1.65}/cooptools/finance/fund_projection.py +0 -0
  60. {cooptools-1.63 → cooptools-1.65}/cooptools/finance/fund_projection_renderer.py +0 -0
  61. {cooptools-1.63 → cooptools-1.65}/cooptools/finance/futureValueProjector.py +0 -0
  62. {cooptools-1.63 → cooptools-1.65}/cooptools/finance/growth_projections.py +0 -0
  63. {cooptools-1.63 → cooptools-1.65}/cooptools/finance/reports.py +0 -0
  64. {cooptools-1.63 → cooptools-1.65}/cooptools/finance/utils.py +0 -0
  65. {cooptools-1.63 → cooptools-1.65}/cooptools/geometry_utils/__init__.py +0 -0
  66. {cooptools-1.63 → cooptools-1.65}/cooptools/geometry_utils/common.py +0 -0
  67. {cooptools-1.63 → cooptools-1.65}/cooptools/geometry_utils/curve_utils.py +0 -0
  68. {cooptools-1.63 → cooptools-1.65}/cooptools/geometry_utils/line_utils.py +0 -0
  69. {cooptools-1.63 → cooptools-1.65}/cooptools/geometry_utils/polygon_utils.py +0 -0
  70. {cooptools-1.63 → cooptools-1.65}/cooptools/geometry_utils/rect_utils.py +0 -0
  71. {cooptools-1.63 → cooptools-1.65}/cooptools/geometry_utils/triangle_utils.py +0 -0
  72. {cooptools-1.63 → cooptools-1.65}/cooptools/geometry_utils/vector_utils.py +0 -0
  73. {cooptools-1.63 → cooptools-1.65}/cooptools/graphs/__init__.py +0 -0
  74. {cooptools-1.63 → cooptools-1.65}/cooptools/graphs/astar_results_viewer.py +0 -0
  75. {cooptools-1.63 → cooptools-1.65}/cooptools/graphs/draw.py +0 -0
  76. {cooptools-1.63 → cooptools-1.65}/cooptools/graphs/graph_dcs.py +0 -0
  77. {cooptools-1.63 → cooptools-1.65}/cooptools/graphs/graph_definitions.py +0 -0
  78. {cooptools-1.63 → cooptools-1.65}/cooptools/graphs/utils.py +0 -0
  79. {cooptools-1.63 → cooptools-1.65}/cooptools/ideas/__init__.py +0 -0
  80. {cooptools-1.63 → cooptools-1.65}/cooptools/ideas/actionItemTracker/__init__.py +0 -0
  81. {cooptools-1.63 → cooptools-1.65}/cooptools/ideas/actionItemTracker/ai_tracker.py +0 -0
  82. {cooptools-1.63 → cooptools-1.65}/cooptools/ideas/actionItemTracker/dcs.py +0 -0
  83. {cooptools-1.63 → cooptools-1.65}/cooptools/ideas/discreteEventSimulator/__init__.py +0 -0
  84. {cooptools-1.63 → cooptools-1.65}/cooptools/ideas/discreteEventSimulator/discreteEventSimulator.py +0 -0
  85. {cooptools-1.63 → cooptools-1.65}/cooptools/ideas/jsonIO.py +0 -0
  86. {cooptools-1.63 → cooptools-1.65}/cooptools/ideas/metrics.py +0 -0
  87. {cooptools-1.63 → cooptools-1.65}/cooptools/ideas/triggerActionSystem/__init__.py +0 -0
  88. {cooptools-1.63 → cooptools-1.65}/cooptools/ideas/triggerActionSystem/signal.py +0 -0
  89. {cooptools-1.63 → cooptools-1.65}/cooptools/ideas/triggerActionSystem/trigger.py +0 -0
  90. {cooptools-1.63 → cooptools-1.65}/cooptools/loggingHelpers.py +0 -0
  91. {cooptools-1.63 → cooptools-1.65}/cooptools/marchingSquares.py +0 -0
  92. {cooptools-1.63 → cooptools-1.65}/cooptools/materialHandling/__init__.py +0 -0
  93. {cooptools-1.63 → cooptools-1.65}/cooptools/materialHandling/cli.py +0 -0
  94. {cooptools-1.63 → cooptools-1.65}/cooptools/materialHandling/dcs.py +0 -0
  95. {cooptools-1.63 → cooptools-1.65}/cooptools/materialHandling/selectionCriteria.py +0 -0
  96. {cooptools-1.63 → cooptools-1.65}/cooptools/matrixManipulation.py +0 -0
  97. {cooptools-1.63 → cooptools-1.65}/cooptools/os_manip.py +0 -0
  98. {cooptools-1.63 → cooptools-1.65}/cooptools/pandasHelpers.py +0 -0
  99. {cooptools-1.63 → cooptools-1.65}/cooptools/physics/__init__.py +0 -0
  100. {cooptools-1.63/cooptools/reservation → cooptools-1.65/cooptools/physics/demo}/__init__.py +0 -0
  101. {cooptools-1.63 → cooptools-1.65}/cooptools/physics/movement.py +0 -0
  102. {cooptools-1.63 → cooptools-1.65}/cooptools/physics/waypoint.py +0 -0
  103. {cooptools-1.63 → cooptools-1.65}/cooptools/plotting.py +0 -0
  104. {cooptools-1.63 → cooptools-1.65}/cooptools/printing.py +0 -0
  105. {cooptools-1.63 → cooptools-1.65}/cooptools/protocols.py +0 -0
  106. {cooptools-1.63 → cooptools-1.65}/cooptools/qualifiers/__init__.py +0 -0
  107. {cooptools-1.63 → cooptools-1.65}/cooptools/qualifiers/cli.py +0 -0
  108. {cooptools-1.63 → cooptools-1.65}/cooptools/qualifiers/qualifier.py +0 -0
  109. {cooptools-1.63 → cooptools-1.65}/cooptools/randoms.py +0 -0
  110. {cooptools-1.63 → cooptools-1.65}/cooptools/register.py +0 -0
  111. {cooptools-1.63/cooptools/statistics/controlChart → cooptools-1.65/cooptools/reservation}/__init__.py +0 -0
  112. {cooptools-1.63 → cooptools-1.65}/cooptools/reservation/dcs.py +0 -0
  113. {cooptools-1.63 → cooptools-1.65}/cooptools/reservation/enums.py +0 -0
  114. {cooptools-1.63 → cooptools-1.65}/cooptools/reservation/reservationmanager.py +0 -0
  115. {cooptools-1.63 → cooptools-1.65}/cooptools/retry.py +0 -0
  116. {cooptools-1.63 → cooptools-1.65}/cooptools/sectors/__init__.py +0 -0
  117. {cooptools-1.63 → cooptools-1.65}/cooptools/sectors/grids/__init__.py +0 -0
  118. {cooptools-1.63 → cooptools-1.65}/cooptools/sectors/grids/gridState.py +0 -0
  119. {cooptools-1.63 → cooptools-1.65}/cooptools/sectors/grids/grid_base.py +0 -0
  120. {cooptools-1.63 → cooptools-1.65}/cooptools/sectors/grids/hexGrid.py +0 -0
  121. {cooptools-1.63 → cooptools-1.65}/cooptools/sectors/grids/rectGrid.py +0 -0
  122. {cooptools-1.63 → cooptools-1.65}/cooptools/sectors/hex_utils.py +0 -0
  123. {cooptools-1.63 → cooptools-1.65}/cooptools/sectors/sect_utils.py +0 -0
  124. {cooptools-1.63 → cooptools-1.65}/cooptools/sectors/sectorTree/__init__.py +0 -0
  125. {cooptools-1.63 → cooptools-1.65}/cooptools/sectors/sectorTree/sectorTree.py +0 -0
  126. {cooptools-1.63 → cooptools-1.65}/cooptools/statistics/__init__.py +0 -0
  127. {cooptools-1.63 → cooptools-1.65}/cooptools/statistics/activityReport.py +0 -0
  128. {cooptools-1.63/cooptools/statistics/oee → cooptools-1.65/cooptools/statistics/controlChart}/__init__.py +0 -0
  129. {cooptools-1.63 → cooptools-1.65}/cooptools/statistics/controlChart/controlChart.py +0 -0
  130. {cooptools-1.63 → cooptools-1.65}/cooptools/statistics/controlChart/plotting.py +0 -0
  131. {cooptools-1.63 → cooptools-1.65}/cooptools/statistics/oee/oeeHub.py +0 -0
  132. {cooptools-1.63 → cooptools-1.65}/cooptools/taskProcessing/__init__.py +0 -0
  133. {cooptools-1.63 → cooptools-1.65}/cooptools/taskProcessing/dcs.py +0 -0
  134. {cooptools-1.63 → cooptools-1.65}/cooptools/taskProcessing/taskProcessor.py +0 -0
  135. {cooptools-1.63 → cooptools-1.65}/cooptools/timeTracker/__init__.py +0 -0
  136. {cooptools-1.63 → cooptools-1.65}/cooptools/timeTracker/decay.py +0 -0
  137. {cooptools-1.63 → cooptools-1.65}/cooptools/timeTracker/timeTracker.py +0 -0
  138. {cooptools-1.63 → cooptools-1.65}/cooptools/timeWindow.py +0 -0
  139. {cooptools-1.63 → cooptools-1.65}/cooptools/toggles.py +0 -0
  140. {cooptools-1.63 → cooptools-1.65}/cooptools/transform.py +0 -0
  141. {cooptools-1.63 → cooptools-1.65}/cooptools/trends.py +0 -0
  142. {cooptools-1.63 → cooptools-1.65}/cooptools/typeProviders.py +0 -0
  143. {cooptools-1.63 → cooptools-1.65}/cooptools/typevalidation.py +0 -0
  144. {cooptools-1.63 → cooptools-1.65}/cooptools/version.py +0 -0
  145. {cooptools-1.63 → cooptools-1.65}/cooptools.egg-info/dependency_links.txt +0 -0
  146. {cooptools-1.63 → cooptools-1.65}/cooptools.egg-info/not-zip-safe +0 -0
  147. {cooptools-1.63 → cooptools-1.65}/cooptools.egg-info/requires.txt +0 -0
  148. {cooptools-1.63 → cooptools-1.65}/cooptools.egg-info/top_level.txt +0 -0
  149. {cooptools-1.63 → cooptools-1.65}/requirements.txt +0 -0
  150. {cooptools-1.63 → cooptools-1.65}/setup.cfg +0 -0
  151. {cooptools-1.63 → cooptools-1.65}/tests/test_CliAtomicUserInteraction.py +0 -0
  152. {cooptools-1.63 → cooptools-1.65}/tests/test_asyncable.py +0 -0
  153. {cooptools-1.63 → cooptools-1.65}/tests/test_cdp_bugs.py +0 -0
  154. {cooptools-1.63 → cooptools-1.65}/tests/test_commandDesignPattern.py +0 -0
  155. {cooptools-1.63 → cooptools-1.65}/tests/test_common.py +0 -0
  156. {cooptools-1.63 → cooptools-1.65}/tests/test_config.py +0 -0
  157. {cooptools-1.63 → cooptools-1.65}/tests/test_coopEnum.py +0 -0
  158. {cooptools-1.63 → cooptools-1.65}/tests/test_currency.py +0 -0
  159. {cooptools-1.63 → cooptools-1.65}/tests/test_date_utils.py +0 -0
  160. {cooptools-1.63 → cooptools-1.65}/tests/test_fund_projection.py +0 -0
  161. {cooptools-1.63 → cooptools-1.65}/tests/test_os_manip.py +0 -0
  162. {cooptools-1.63 → cooptools-1.65}/tests/test_pandas.py +0 -0
  163. {cooptools-1.63 → cooptools-1.65}/tests/test_printing.py +0 -0
  164. {cooptools-1.63 → cooptools-1.65}/tests/test_qualifiers.py +0 -0
  165. {cooptools-1.63 → cooptools-1.65}/tests/test_sectors.py +0 -0
  166. {cooptools-1.63 → cooptools-1.65}/tests/test_sectors_hex.py +0 -0
  167. {cooptools-1.63 → cooptools-1.65}/tests/test_sectors_sectorTree.py +0 -0
  168. {cooptools-1.63 → cooptools-1.65}/tests/test_statistics.py +0 -0
  169. {cooptools-1.63 → cooptools-1.65}/tests/test_tasks.py +0 -0
  170. {cooptools-1.63 → cooptools-1.65}/tests/test_toggles.py +0 -0
  171. {cooptools-1.63 → cooptools-1.65}/tests/test_trends.py +0 -0
  172. {cooptools-1.63 → cooptools-1.65}/tests/test_typevalidation.py +0 -0
  173. {cooptools-1.63 → cooptools-1.65}/tests/test_vector_utils.py +0 -0
  174. {cooptools-1.63 → cooptools-1.65}/tests/test_version.py +0 -0
  175. {cooptools-1.63 → cooptools-1.65}/tests/tests_graph.py +0 -0
  176. {cooptools-1.63 → cooptools-1.65}/tests/tests_gridsystem.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cooptools
3
- Version: 1.63
3
+ Version: 1.65
4
4
  Summary: Generic Tooling
5
5
  Home-page: https://github.com/tylertjburns/cooptools
6
6
  Author: tburns
@@ -2,7 +2,8 @@ import functools
2
2
  import time
3
3
  import logging
4
4
  import inspect
5
- from typing import Protocol, Dict
5
+ import threading
6
+ from typing import Protocol, Dict, Optional
6
7
  from typing_extensions import runtime_checkable
7
8
 
8
9
  @runtime_checkable
@@ -15,6 +16,22 @@ class TimeableProtocol(Protocol):
15
16
 
16
17
  TIMEABLE_LOOKUPATTR = "_timeable"
17
18
 
19
+ # ---------------------------------------------------------------------------
20
+ # Thread-local call stack — tracks the currently executing @timer chain so
21
+ # each invocation knows its parent function. Push before calling func, pop
22
+ # after (even on exception) so the stack stays consistent.
23
+ # ---------------------------------------------------------------------------
24
+
25
+ _call_stack_local = threading.local()
26
+
27
+
28
+ def get_call_stack() -> list:
29
+ """Return the current thread's timer call stack (mutable live reference)."""
30
+ if not hasattr(_call_stack_local, 'stack'):
31
+ _call_stack_local.stack = []
32
+ return _call_stack_local.stack
33
+
34
+
18
35
  def try_handler(logger: logging.Logger = None):
19
36
  def wrapper(func):
20
37
  @functools.wraps(func)
@@ -32,7 +49,8 @@ def try_handler(logger: logging.Logger = None):
32
49
  return wrapper
33
50
 
34
51
 
35
- def update_ttc_values(ttc: TimeableProtocol, func_name: str, run_time: float):
52
+ def update_ttc_values(ttc: TimeableProtocol, func_name: str, run_time: float,
53
+ parent: Optional[str] = None):
36
54
  #update counter
37
55
  ttc.ns.setdefault(func_name, 0)
38
56
  ttc.ns[func_name] += 1
@@ -55,6 +73,10 @@ def update_ttc_values(ttc: TimeableProtocol, func_name: str, run_time: float):
55
73
  my_avg = ttc.internally_tracked_times[func_name] / ttc.ns[func_name]
56
74
  ttc.avgs[func_name] = my_avg
57
75
 
76
+ # Record call-chain parent once (hierarchy is static; first writer wins).
77
+ if parent is not None and hasattr(ttc, 'parents'):
78
+ ttc.parents.setdefault(func_name, parent)
79
+
58
80
 
59
81
  def timer(logger: logging.Logger = None,
60
82
  log_level: int = logging.DEBUG,
@@ -68,11 +90,20 @@ def timer(logger: logging.Logger = None,
68
90
 
69
91
  @functools.wraps(func)
70
92
  def wrapper_timer(*args, **kwargs):
71
- # capture time
93
+ # Push to call stack before executing so nested @timer calls see us
94
+ # as their parent. Pop in finally so the stack stays consistent
95
+ # even if func raises.
96
+ _stack = get_call_stack()
97
+ _parent: Optional[str] = _stack[-1] if _stack else None
98
+ _stack.append(func.__name__)
99
+
72
100
  start_time = time.perf_counter() # 1
73
- value = func(*args, **kwargs)
74
- end_time = time.perf_counter() # 2
75
- run_time = end_time - start_time # 3
101
+ try:
102
+ value = func(*args, **kwargs)
103
+ finally:
104
+ end_time = time.perf_counter() # 2
105
+ run_time = end_time - start_time # 3
106
+ _stack.pop()
76
107
 
77
108
  ttc = None
78
109
 
@@ -96,7 +127,8 @@ def timer(logger: logging.Logger = None,
96
127
  if ttc is None:
97
128
  return value
98
129
 
99
- update_ttc_values(ttc, func_name=func.__name__, run_time=run_time)
130
+ update_ttc_values(ttc, func_name=func.__name__, run_time=run_time,
131
+ parent=_parent)
100
132
  end_time2 = time.perf_counter()
101
133
  timer_runtime = end_time2 - end_time
102
134
  update_ttc_values(ttc, func_name=TIMEABLE_LOOKUPATTR, run_time=timer_runtime)
@@ -1,4 +1,5 @@
1
- from typing import Tuple
1
+ from dataclasses import dataclass
2
+ from typing import Tuple, Dict
2
3
  from cooptools.common import verify_unique, verify_len, rads_to_degrees, degree_to_rads
3
4
  from cooptools.geometry_utils.vector_utils import interpolate, orthogonal2x2, vector_between, add_vectors, \
4
5
  distance_between, zero_vector
@@ -8,8 +9,48 @@ import cooptools.geometry_utils.line_utils as line
8
9
  from cooptools.geometry_utils import common as cmn
9
10
  from cooptools.geometry_utils.common import CircleCenterRadius
10
11
  import math
12
+ import numpy as np
11
13
  import random as rnd
12
- from typing import Dict
14
+
15
+
16
+ @dataclass
17
+ class CircleTrajectoryConstants:
18
+ """
19
+ Pre-computed l-independent geometric constants for one circle-arc trajectory variant.
20
+
21
+ Describes a three-segment path:
22
+ x0 -> x1 straight, length = l
23
+ x1 -> x2 circular arc, length = r(l) * theta
24
+ x2 -> xT straight, length = |xT - x2(l)|
25
+
26
+ All fields are derived from fixed geometry (positions and headings) and are
27
+ independent of l, so they are computed once and reused for any value of l.
28
+
29
+ Radius (piecewise-linear in l):
30
+ t(l) = A + B*l signed perpendicular offset from x1 to the circle center
31
+ r(l) = |t(l)| circle radius
32
+
33
+ Exit point (affine in l):
34
+ x2(l) = p + l*q p and q are constant 2D vectors
35
+
36
+ Final segment length (square-root quadratic in l):
37
+ f = xT - p
38
+ |xT - x2(l)| = |f - l*q| = sqrt(f_sq - 2*l*f_dot_q + l^2*q_sq)
39
+
40
+ Arc angles (always sum to 2*pi):
41
+ theta_direct: CCW arc from x1 to x2
42
+ theta_complement: CW arc from x1 to x2
43
+ """
44
+ A: float # constant offset in t(l) = A + B*l
45
+ B: float # slope in t(l) = A + B*l
46
+ theta_direct: float # CCW arc angle [radians]
47
+ theta_complement: float # CW arc angle [radians]
48
+ p: np.ndarray # x2(l=0), base point of the exit position
49
+ q: np.ndarray # dx2/dl, direction the exit point moves as l increases
50
+ f: np.ndarray # xT - p, constant vector used for the final segment
51
+ f_dot_q: float # dot(f, q), precomputed
52
+ q_sq: float # |q|^2, precomputed
53
+ f_sq: float # |f|^2, precomputed
13
54
 
14
55
 
15
56
  def from_boundary_points(a: Tuple[float, float], b: Tuple[float, float], c: Tuple[float, float]) -> CircleCenterRadius:
@@ -104,6 +145,121 @@ def rotated_point(point: cmn.FloatVec,
104
145
  return x2
105
146
 
106
147
 
148
+ def circle_constants_for_bisector(
149
+ x0: vec.FloatVec,
150
+ xT: vec.FloatVec,
151
+ O: vec.FloatVec,
152
+ u0: vec.FloatVec,
153
+ u0_perp: vec.FloatVec,
154
+ u_b: vec.FloatVec,
155
+ rotation_rads: float,
156
+ ) -> CircleTrajectoryConstants:
157
+ """
158
+ Derive closed-form constants that express circle radius and exit point as functions of l.
159
+
160
+ Context:
161
+ A trajectory enters at x0 with unit heading u0, travels a straight distance l to
162
+ x1 = x0 + l*u0, then follows a circular arc to exit point x2, then travels in a
163
+ straight line to xT. The circle center C lies on both:
164
+ (1) the perpendicular to u0 through x1, and
165
+ (2) a bisector ray from O in direction u_b.
166
+
167
+ This function converts the geometry of finding that circle (normally done by intersecting
168
+ two lines) into linear algebraic coefficients so that r(l) and x2(l) can be evaluated
169
+ directly without recomputing intersections.
170
+
171
+ DERIVING t(l) — signed offset from x1 to the circle center along u0_perp:
172
+
173
+ C must satisfy two parametric equations simultaneously:
174
+ C = x1 + t * u0_perp (perpendicular through x1)
175
+ C = O + s * u_b (bisector ray from O)
176
+
177
+ Substituting x1 = x0 + l*u0 and setting the two equal:
178
+ x0 + l*u0 + t*u0_perp = O + s*u_b
179
+
180
+ Take the 2D cross product of both sides with u_b (eliminates s, since u_b x u_b = 0):
181
+ (x0 - O) x u_b + l*(u0 x u_b) + t*(u0_perp x u_b) = 0
182
+
183
+ Solve for t:
184
+ t(l) = [ (O - x0) x u_b - l*(u0 x u_b) ] / (u0_perp x u_b)
185
+ = A + B*l
186
+
187
+ Where (D = u0_perp x u_b as the shared denominator):
188
+ A = (O - x0) x u_b / D (constant offset — value of t when l=0)
189
+ B = -(u0 x u_b) / D (slope — rate of change of t with l)
190
+
191
+ The radius r(l) = |t(l)| is piecewise-linear in l, with a kink at t=0.
192
+
193
+ DERIVING x2(l) — the arc exit point:
194
+
195
+ Since C = x1 + t*u0_perp and x2 is x1 rotated around C by rotation_rads:
196
+ x2 = C + R(rotation_rads) * (x1 - C)
197
+ = C - t * R(rotation_rads) * u0_perp [x1 - C = -t * u0_perp]
198
+ = x1 + t * u0_perp - t * R * u0_perp
199
+ = x1 + t * (u0_perp - R * u0_perp)
200
+
201
+ Substituting x1 = x0 + l*u0 and t = A + B*l:
202
+ x2(l) = x0 + l*u0 + (A + B*l) * w where w = u0_perp - R(rotation_rads)*u0_perp
203
+ = (x0 + A*w) + l*(u0 + B*w)
204
+ = p + l*q
205
+
206
+ This affine-in-l result means x2 slides along a straight line as l varies —
207
+ a key structural property that makes the full trajectory length analytically tractable.
208
+
209
+ Args:
210
+ x0: Initial position (start of trajectory).
211
+ xT: Target position (end of trajectory).
212
+ O: Intersection point of the two heading rays.
213
+ u0: Unit initial heading vector.
214
+ u0_perp: Vector perpendicular to u0 (any 90-degree rotation; sign does not affect result).
215
+ u_b: Unit direction of the bisector ray from O (determines which circle family).
216
+ rotation_rads: CCW rotation angle [radians] applied to x1 around C to reach x2.
217
+
218
+ Returns:
219
+ CircleTrajectoryConstants with all l-independent coefficients precomputed.
220
+ """
221
+ x0_np = np.array(x0, dtype=float)
222
+ xT_np = np.array(xT, dtype=float)
223
+ O_np = np.array(O, dtype=float)
224
+ u0_np = np.array(u0, dtype=float)
225
+ u0_perp_np = np.array(u0_perp, dtype=float)
226
+ u_b_np = np.array(u_b, dtype=float)
227
+
228
+ # 2D cross product: a x b = a[0]*b[1] - a[1]*b[0] (scalar z-component).
229
+ # Computed manually rather than via np.cross, which dropped support for
230
+ # 2D input vectors (deprecated in NumPy 2.0, removed in later 2.x releases).
231
+ def _cross_2d(a: np.ndarray, b: np.ndarray) -> float:
232
+ return float(a[0] * b[1] - a[1] * b[0])
233
+
234
+ D = _cross_2d(u0_perp_np, u_b_np) # denominator, constant
235
+ A = _cross_2d(O_np - x0_np, u_b_np) / D # t-offset when l=0
236
+ B = -_cross_2d(u0_np, u_b_np) / D # dt/dl
237
+
238
+ # w encodes how the exit point shifts relative to t (and therefore l).
239
+ # rotated_point with default center=(0,0) applies a pure vector rotation.
240
+ rotated_u0_perp = np.array(rotated_point(tuple(u0_perp_np), rads=rotation_rads))
241
+ w = u0_perp_np - rotated_u0_perp # constant 2D vector: u0_perp - R(rotation_rads)*u0_perp
242
+
243
+ p = x0_np + A * w # x2(l=0)
244
+ q = u0_np + B * w # dx2/dl
245
+ f = xT_np - p # constant vector: |xT - x2(l)| = |f - l*q|
246
+
247
+ # CCW arc from x1 to x2 subtends rotation_rads mod 2*pi;
248
+ # CW arc takes the remainder to complete the full circle.
249
+ theta_direct = rotation_rads % (2 * math.pi)
250
+ theta_complement = 2 * math.pi - theta_direct
251
+
252
+ return CircleTrajectoryConstants(
253
+ A=A, B=B,
254
+ theta_direct=theta_direct,
255
+ theta_complement=theta_complement,
256
+ p=p, q=q, f=f,
257
+ f_dot_q=float(np.dot(f, q)),
258
+ q_sq=float(np.dot(q, q)),
259
+ f_sq=float(np.dot(f, f)),
260
+ )
261
+
262
+
107
263
  def random_point_on_circle(center: vec.FloatVec, radius: float) -> vec.FloatVec:
108
264
  rads = rnd.uniform(0, math.pi * 2)
109
265
  return point_at_angle(center, radius, radians=rads)
@@ -15,6 +15,7 @@ from cooptools.sectors import sectorTree as st
15
15
  from cooptools.graphs import utils as u
16
16
  from dataclasses import dataclass
17
17
  from functools import lru_cache
18
+ import heapq
18
19
  import json
19
20
  from cooptools.graphs.graph_dcs import *
20
21
 
@@ -663,7 +664,8 @@ class Graph(object):
663
664
  h_func: Callable[[Node, Node], float] = None,
664
665
  ignored_disablers:frozenset[str]=None,
665
666
  disabled_node_ids:frozenset[str] = None,
666
- early_out_cost: float = None) -> AStarResults:
667
+ early_out_cost: float = None,
668
+ collect_steps: bool = False) -> AStarResults:
667
669
 
668
670
  if disabled_node_ids is not None:
669
671
  if ((s_in:=(start.name in disabled_node_ids)) or
@@ -684,30 +686,37 @@ class Graph(object):
684
686
 
685
687
  steps = {}
686
688
 
687
- # Initialize both open and closed list
688
- open_set = set()
689
+ # open_heap: min-heap of (f, counter, AStarMetrics) — O(log N) extraction
690
+ # open_nodes: {node_name -> AStarMetrics} — O(1) lookup; authoritative membership
691
+ # Lazy deletion: heap entries whose node_name is no longer in open_nodes are stale and skipped.
692
+ open_heap = []
693
+ open_nodes = {}
689
694
  closed_set = set()
695
+ _heap_counter = 0
690
696
 
691
697
  # Check reachability: any enabled edge into end whose source isn't disabled
692
698
  connections_to_end = self.edges_to_node(end, only_enabled=True, ignored_disablers=ignored_disablers)
693
699
  if disabled_node_ids is not None:
694
700
  connections_to_end = [e for e in connections_to_end if e.start.name not in disabled_node_ids]
695
701
  if len(connections_to_end) != 0:
696
- open_set.add(start_iter)
702
+ heapq.heappush(open_heap, (0.0, _heap_counter, start_iter))
703
+ open_nodes[start_iter.graph_node.name] = start_iter
697
704
 
698
705
  cc = -1
699
706
 
700
707
  results = None
701
708
  _early_out_triggered = False
702
709
  # Loop until you find the end
703
- while len(open_set) > 0:
710
+ while open_heap:
704
711
  cc += 1
705
712
 
706
- # Find the node on open list with the least F value
707
- current_item = next(iter(open_set))
708
- for open_item in open_set:
709
- if open_item.f < current_item.f:
710
- current_item = open_item
713
+ _, _, current_item = heapq.heappop(open_heap)
714
+
715
+ # Lazy deletion: skip heap entries superseded by a better path
716
+ if open_nodes.get(current_item.graph_node.name) is not current_item:
717
+ continue
718
+
719
+ del open_nodes[current_item.graph_node.name]
711
720
 
712
721
  # Early-out: f is a lower bound on total path cost through current_item.
713
722
  # If the minimum f in the open set already meets or exceeds the caller's
@@ -716,10 +725,9 @@ class Graph(object):
716
725
  _early_out_triggered = True
717
726
  break
718
727
 
719
- steps[cc] = {"open_set": set(open_set), "closed_set": set(closed_set), "current_item": current_item}
728
+ if collect_steps:
729
+ steps[cc] = {"open_set": set(open_nodes.values()), "closed_set": set(closed_set), "current_item": current_item}
720
730
 
721
- # Pop current off open list, add to closed list
722
- open_set.remove(current_item)
723
731
  closed_set.add(current_item)
724
732
 
725
733
  # Found the goal
@@ -765,19 +773,26 @@ class Graph(object):
765
773
  if new_item in closed_set:
766
774
  continue
767
775
 
768
- open_set.add(new_item)
769
-
770
- # calculate new g, h, f from current pivot node to the new node
776
+ # calculate new g, h, f before deciding whether to push
771
777
  calc_g = current_item.g + (g_func(current_item.graph_node, new_node) if g_func else min_edge.cost)
772
778
  calc_h = h_func(new_node,
773
779
  end_iter.graph_node) if h_func else vec.distance_between(new_node.pos, end_iter.graph_node.pos)
774
780
  calc_f = calc_g + calc_h
775
781
 
782
+ # Skip if we already have a path to this node that's at least as good
783
+ existing = open_nodes.get(new_node.name)
784
+ if existing is not None and existing.g <= calc_g:
785
+ continue
786
+
776
787
  new_item.parent = current_item
777
788
  new_item.g = calc_g
778
789
  new_item.h = calc_h
779
790
  new_item.f = calc_f
780
791
 
792
+ _heap_counter += 1
793
+ heapq.heappush(open_heap, (calc_f, _heap_counter, new_item))
794
+ open_nodes[new_node.name] = new_item
795
+
781
796
  if results is None:
782
797
  if _early_out_triggered:
783
798
  logger.debug(f"A* early-out from [{start}] to [{end}]: f >= early_out_cost ({early_out_cost})")
@@ -0,0 +1,136 @@
1
+ """
2
+ Local demo server for GoalSeeker / WaypointFollower.
3
+
4
+ Runs the *real* cooptools.physics classes and returns recorded frame-by-frame
5
+ trajectories over HTTP so the accompanying index.html can replay genuine output --
6
+ no physics is reimplemented in JavaScript.
7
+
8
+ Usage (from the repo root, with cooptools importable):
9
+ python -m cooptools.physics.demo.server
10
+ (or) python cooptools/physics/demo/server.py
11
+
12
+ Then open the printed URL (defaults to http://127.0.0.1:8765).
13
+ """
14
+ import json
15
+ import http.server
16
+ import socketserver
17
+ import webbrowser
18
+ from pathlib import Path
19
+
20
+ from cooptools.physics.waypointFollower import WaypointFollower
21
+ from cooptools.physics.waypoint import Waypoint
22
+ from cooptools.physics.movement import Velocity
23
+ from cooptools.transform import Transform
24
+
25
+ DEMO_DIR = Path(__file__).parent
26
+ INDEX_FILE = DEMO_DIR / "index.html"
27
+
28
+
29
+ def run_simulation(payload: dict) -> dict:
30
+ start_pos = tuple(payload["start_pos"])
31
+ start_vel = tuple(payload.get("start_vel", (0.0, 0.0)))
32
+ max_accel = float(payload["max_accel"])
33
+ realign = bool(payload.get("realign", True))
34
+ dt_ms = int(payload.get("dt_ms", 16))
35
+ max_steps = int(payload.get("max_steps", 20000))
36
+ # GoalSeeker's library defaults (0.001) assume a much smaller unit scale than
37
+ # canvas pixels, and simultaneous multi-axis arrivals (a diagonal destination)
38
+ # only settle to a small bounded residual, not an arbitrarily tight one -- see
39
+ # arrival_acceleration's docstring. A couple of pixels is a perfectly "arrived"
40
+ # demo and avoids waiting on a residual that may never fully vanish.
41
+ close_enough_tolerance = float(payload.get("close_enough_tolerance", 2.0))
42
+ slow_enough_tolerance = float(payload.get("slow_enough_tolerance", 2.0))
43
+
44
+ follower = WaypointFollower(
45
+ name="demo",
46
+ initial_transform=Transform(translation=start_pos),
47
+ max_acceleration=max_accel,
48
+ initial_velocity=Velocity(initial_m_s_vec=start_vel),
49
+ )
50
+ follower.add_waypoints([
51
+ Waypoint(end_pos=tuple(wp["pos"]), is_destination=bool(wp.get("is_destination", False)))
52
+ for wp in payload["waypoints"]
53
+ ])
54
+
55
+ def snapshot(t_ms, reached=None, target_was_destination=None):
56
+ return {
57
+ "t_ms": t_ms,
58
+ "pos": list(follower.Position),
59
+ "vel": list(follower.Velocity.CurrentVelocity_M_S),
60
+ "acc": list(follower.Acceleration.CurrentAccel_M_S),
61
+ "reached": [{"pos": list(w.end_pos), "is_destination": w.is_destination} for w in (reached or [])],
62
+ "target_was_destination": target_was_destination,
63
+ }
64
+
65
+ frames = [snapshot(0)]
66
+ t_ms = 0
67
+ for _ in range(max_steps):
68
+ if not follower.waypoints:
69
+ break
70
+ target_was_destination = follower.CurrentTarget.is_destination
71
+ reached = follower.update(
72
+ delta_time_ms=dt_ms,
73
+ realign_to_next_waypoint=realign,
74
+ close_enough_tolerance=close_enough_tolerance,
75
+ slow_enough_tolerance=slow_enough_tolerance,
76
+ )
77
+ t_ms += dt_ms
78
+ frames.append(snapshot(t_ms, reached, target_was_destination))
79
+
80
+ return {"frames": frames, "completed": len(follower.waypoints) == 0}
81
+
82
+
83
+ class DemoHandler(http.server.BaseHTTPRequestHandler):
84
+ def _send_json(self, status: int, payload: dict):
85
+ body = json.dumps(payload).encode("utf-8")
86
+ self.send_response(status)
87
+ self.send_header("Content-Type", "application/json")
88
+ self.send_header("Content-Length", str(len(body)))
89
+ self.end_headers()
90
+ self.wfile.write(body)
91
+
92
+ def do_GET(self):
93
+ if self.path in ("/", "/index.html"):
94
+ body = INDEX_FILE.read_bytes()
95
+ self.send_response(200)
96
+ self.send_header("Content-Type", "text/html; charset=utf-8")
97
+ self.send_header("Content-Length", str(len(body)))
98
+ self.end_headers()
99
+ self.wfile.write(body)
100
+ else:
101
+ self.send_error(404)
102
+
103
+ def do_POST(self):
104
+ if self.path != "/simulate":
105
+ self.send_error(404)
106
+ return
107
+ try:
108
+ length = int(self.headers.get("Content-Length", 0))
109
+ payload = json.loads(self.rfile.read(length))
110
+ result = run_simulation(payload)
111
+ self._send_json(200, result)
112
+ except Exception as e:
113
+ self._send_json(400, {"error": str(e)})
114
+
115
+ def log_message(self, format, *args):
116
+ pass # quiet console; the simulation output is the point, not access logs
117
+
118
+
119
+ class _Server(socketserver.ThreadingTCPServer):
120
+ allow_reuse_address = True
121
+
122
+
123
+ def main(port: int = 8765):
124
+ with _Server(("127.0.0.1", port), DemoHandler) as httpd:
125
+ url = f"http://127.0.0.1:{port}"
126
+ print(f"Seek/Arrive demo server running at {url}")
127
+ print("Ctrl+C to stop.")
128
+ webbrowser.open(url)
129
+ try:
130
+ httpd.serve_forever()
131
+ except KeyboardInterrupt:
132
+ pass
133
+
134
+
135
+ if __name__ == "__main__":
136
+ main()
@@ -203,11 +203,13 @@ class GoalSeeker:
203
203
  close_enough_tolerance: float = 0.001,
204
204
  slow_enough_tolerance: float = 0.001,
205
205
  stop_rotating_threshold: int = 10,
206
- velocity_magnitude_bounds=None
206
+ velocity_magnitude_bounds=None,
207
+ continue_after_goal_reached: bool = False,
208
+ decel_lookahead_distance: float = None
207
209
  ) -> bool:
208
210
  verify_val(delta_time_ms, gte=0, not_none=True)
209
211
 
210
- self._last_pos = self._mover.Transform.Translation.Vector
212
+ last_pos = self._mover.Transform.Translation.Vector
211
213
  self._mover.update(delta_time_ms=delta_time_ms,
212
214
  stop_rotating_threshold=stop_rotating_threshold,
213
215
  velocity_magnitude_bounds=velocity_magnitude_bounds)
@@ -219,33 +221,109 @@ class GoalSeeker:
219
221
  elif self._goal is None:
220
222
  self._goal = self._mover.Transform.Translation.Vector
221
223
 
222
- ''' Calculate Vector to goal'''
223
- vector_to_goal = vec.vector_between(self._mover.Transform.Translation.Vector, self._goal)
224
+ current_pos = self._mover.Transform.Translation.Vector
224
225
 
225
- ''' Return early once "close enough"'''
226
- close = vec.vector_len(vector_to_goal) <= close_enough_tolerance
226
+ ''' Calculate Vector to goal'''
227
+ vector_to_goal = vec.vector_between(current_pos, self._goal)
228
+
229
+ ''' "Close enough" is checked against the closest point on the segment traveled
230
+ this update (last_pos -> current_pos), not just the endpoint. Otherwise a large
231
+ delta_time_ms can carry the mover clean past the goal in a single step without
232
+ either endpoint ever landing inside close_enough_tolerance, silently missing it. '''
233
+ projected_closest = vec.project_onto(a=self._goal, b=current_pos, origin=last_pos)
234
+ if vec.bounded_by(projected_closest, last_pos, current_pos):
235
+ closest_point_this_step = projected_closest
236
+ else:
237
+ closest_point_this_step = min(
238
+ (last_pos, current_pos),
239
+ key=lambda p: vec.distance_between(p, self._goal)
240
+ )
241
+ close = vec.distance_between(closest_point_this_step, self._goal) <= close_enough_tolerance
227
242
  stopped = self._mover.Velocity.Magnitude <= slow_enough_tolerance
228
243
 
244
+ if continue_after_goal_reached:
245
+ if close:
246
+ logger.info(f'Mover: {self.Name} passed through goal: {self._goal}')
247
+ self._mover.Acceleration.set(m_s2_vec=vec.zero_vector(len(vector_to_goal)))
248
+ return True
249
+
250
+ if decel_lookahead_distance is not None:
251
+ ''' Passing through this goal, but a real stop is coming up later along
252
+ the path: brake based on the TOTAL remaining distance to that eventual
253
+ stop (supplied by the caller), not the distance to this waypoint alone --
254
+ otherwise speed built up seeking through several pass-through waypoints
255
+ is "locked in" with no anticipation of the stop, and can arrive at the
256
+ final destination far too fast to avoid a large overshoot.
257
+
258
+ This is a single scalar accelerate/brake decision (speed vs. remaining
259
+ path distance), not a per-axis one: while accelerating, push toward this
260
+ waypoint same as plain seek; while braking, decelerate directly against
261
+ the current velocity vector. A per-axis version (independently deciding
262
+ each axis via arrival_acceleration) lets one axis start braking before the
263
+ other, since each reaches its own switching threshold at a different
264
+ time -- the combined force then no longer points along the direction of
265
+ travel, curving the mover off its path entirely. '''
266
+ speed = self.Velocity.Magnitude
267
+ scalar_accel = kin.scalar_lookahead_speed_accel(
268
+ remaining_distance=decel_lookahead_distance,
269
+ speed=speed,
270
+ a=self.Acceleration.Max_Magnitude,
271
+ dt=delta_time_ms / 1000.0,
272
+ )
273
+ accel_direction = vector_to_goal if scalar_accel >= 0 else self.Velocity.CurrentVelocity_M_S
274
+ self._mover.Acceleration.set(m_s2_vec=vec.scaled_to_length(accel_direction, scalar_accel))
275
+ return False
276
+
277
+ ''' Seek (not arrive): push at max acceleration straight toward the goal.
278
+ Unlike decide_how_to_accel_decel_towards_a_goal_without_overshoot, this never
279
+ brakes in anticipation of stopping here -- continue_after_goal_reached means
280
+ we don't intend to stop at this target, so there's nothing to avoid overshooting. '''
281
+ self._mover.Acceleration.set(m_s2_vec=vec.scaled_to_length(
282
+ vector_to_goal,
283
+ self.Acceleration.Max_Magnitude
284
+ ))
285
+ return False
286
+
287
+ ''' A tiny fixed epsilon, not close_enough_tolerance/slow_enough_tolerance --
288
+ this only guards against floating-point noise on an axis that's already
289
+ essentially exactly arrived. Using the caller's (potentially loose, realistic)
290
+ arrival tolerance here instead lets one axis independently freeze to zero
291
+ velocity well before the object has actually arrived as a whole, so a diagonal
292
+ approach looks like it abruptly stops on one axis while still sliding on the
293
+ other, rather than a single smooth, simultaneous decel-to-stop. '''
229
294
  for ii in range(len(self.Velocity.CurrentVelocity_M_S)):
230
- if (self.Velocity.CurrentVelocity_M_S[ii] < slow_enough_tolerance and
231
- abs(vector_to_goal[ii]) < close_enough_tolerance):
295
+ if (self.Velocity.CurrentVelocity_M_S[ii] < 1e-6 and
296
+ abs(vector_to_goal[ii]) < 1e-6):
232
297
  self.Velocity.stop_in_idx(ii)
233
298
  self.Acceleration.stop_in_idx(ii)
234
299
  self.Transform.Translation.update(idx_val_map={ii: self._goal[ii]})
235
300
 
301
+ ''' The per-axis snap above can force-align every axis to the goal (each axis
302
+ individually within tolerance) even when the overall close/stopped check above
303
+ -- computed before the snap -- hadn't yet agreed (a diagonal case can satisfy
304
+ the looser per-axis criterion before the stricter combined-vector one). Without
305
+ refreshing here, the stale pre-snap vector_to_goal below computes a residual
306
+ acceleration that immediately nudges the mover away from the position it was
307
+ just snapped to, so "reached" can end up never firing. '''
308
+ current_pos = self._mover.Transform.Translation.Vector
309
+ vector_to_goal = vec.vector_between(current_pos, self._goal)
310
+ close = close or vec.distance_between(current_pos, self._goal) <= close_enough_tolerance
311
+ stopped = self._mover.Velocity.Magnitude <= slow_enough_tolerance
312
+
236
313
  if close and stopped:
237
314
  logger.info(f'Mover: {self.Name} has reached goal: {self._goal}')
238
315
  self._mover.update(
239
- set_pos=goal_pos
316
+ set_pos=self._goal
240
317
  )
241
318
  self._mover.stop()
242
319
  return True
243
320
 
244
321
  ''' Update Velocity '''
245
- self._mover.Acceleration.set(m_s2_vec=kin.decide_how_to_accel_decel_towards_a_goal_without_overshoot(
322
+ self._mover.Acceleration.set(m_s2_vec=kin.arrival_acceleration(
246
323
  vector_to_goal=vector_to_goal,
247
324
  velocity=self.Velocity.CurrentVelocity_M_S,
248
325
  accel_magnitude=self.Acceleration.Max_Magnitude,
326
+ dt=delta_time_ms / 1000.0,
249
327
  ))
250
328
 
251
329
  return False