cooptools 1.64__tar.gz → 1.66__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 (173) hide show
  1. {cooptools-1.64 → cooptools-1.66}/PKG-INFO +1 -1
  2. {cooptools-1.64 → cooptools-1.66}/cooptools/decor.py +39 -7
  3. {cooptools-1.64 → cooptools-1.66}/cooptools/physics/kinematic_utils.py +8 -0
  4. {cooptools-1.64 → cooptools-1.66}/cooptools.egg-info/PKG-INFO +1 -1
  5. {cooptools-1.64 → cooptools-1.66}/setup.py +1 -1
  6. {cooptools-1.64 → cooptools-1.66}/tests/test_kinematic_utils.py +27 -0
  7. {cooptools-1.64 → cooptools-1.66}/tests/test_waypointFollower.py +18 -0
  8. {cooptools-1.64 → cooptools-1.66}/MANIFEST.in +0 -0
  9. {cooptools-1.64 → cooptools-1.66}/README.md +0 -0
  10. {cooptools-1.64 → cooptools-1.66}/cooptools/__init__.py +0 -0
  11. {cooptools-1.64 → cooptools-1.66}/cooptools/anchor.py +0 -0
  12. {cooptools-1.64 → cooptools-1.66}/cooptools/asyncable.py +0 -0
  13. {cooptools-1.64 → cooptools-1.66}/cooptools/cli/CliAtomicUserInteraction.py +0 -0
  14. {cooptools-1.64 → cooptools-1.66}/cooptools/cli/CliMenu.py +0 -0
  15. {cooptools-1.64 → cooptools-1.66}/cooptools/cli/__init__.py +0 -0
  16. {cooptools-1.64 → cooptools-1.66}/cooptools/cli/fileContentReturn.py +0 -0
  17. {cooptools-1.64 → cooptools-1.66}/cooptools/cnxn_info.py +0 -0
  18. {cooptools-1.64 → cooptools-1.66}/cooptools/colors.py +0 -0
  19. {cooptools-1.64 → cooptools-1.66}/cooptools/commandDesignPattern/__init__.py +0 -0
  20. {cooptools-1.64 → cooptools-1.66}/cooptools/commandDesignPattern/commandController.py +0 -0
  21. {cooptools-1.64 → cooptools-1.66}/cooptools/commandDesignPattern/commandProtocol.py +0 -0
  22. {cooptools-1.64 → cooptools-1.66}/cooptools/commandDesignPattern/exceptions.py +0 -0
  23. {cooptools-1.64 → cooptools-1.66}/cooptools/common.py +0 -0
  24. {cooptools-1.64 → cooptools-1.66}/cooptools/config.py +0 -0
  25. {cooptools-1.64 → cooptools-1.66}/cooptools/coopDataclass.py +0 -0
  26. {cooptools-1.64 → cooptools-1.66}/cooptools/coopEnum.py +0 -0
  27. {cooptools-1.64 → cooptools-1.66}/cooptools/coopthreading.py +0 -0
  28. {cooptools-1.64 → cooptools-1.66}/cooptools/currency.py +0 -0
  29. {cooptools-1.64 → cooptools-1.66}/cooptools/dataRefresher/__init__.py +0 -0
  30. {cooptools-1.64 → cooptools-1.66}/cooptools/dataRefresher/dataHub.py +0 -0
  31. {cooptools-1.64 → cooptools-1.66}/cooptools/dataRefresher/dataRefresher.py +0 -0
  32. {cooptools-1.64 → cooptools-1.66}/cooptools/dataStore/__init__.py +0 -0
  33. {cooptools-1.64 → cooptools-1.66}/cooptools/dataStore/dataProcessor.py +0 -0
  34. {cooptools-1.64 → cooptools-1.66}/cooptools/dataStore/dataStoreProtocol.py +0 -0
  35. {cooptools-1.64 → cooptools-1.66}/cooptools/dataStore/dbConnectionURI.py +0 -0
  36. {cooptools-1.64 → cooptools-1.66}/cooptools/dataStore/inMemoryDataStore.py +0 -0
  37. {cooptools-1.64 → cooptools-1.66}/cooptools/date_utils.py +0 -0
  38. {cooptools-1.64 → cooptools-1.66}/cooptools/decay.py +0 -0
  39. {cooptools-1.64 → cooptools-1.66}/cooptools/dictPolicies.py +0 -0
  40. {cooptools-1.64 → cooptools-1.66}/cooptools/exceptions.py +0 -0
  41. {cooptools-1.64 → cooptools-1.66}/cooptools/expertise/__init__.py +0 -0
  42. {cooptools-1.64 → cooptools-1.66}/cooptools/expertise/expertiseArgs.py +0 -0
  43. {cooptools-1.64 → cooptools-1.66}/cooptools/expertise/expertiseSchedules.py +0 -0
  44. {cooptools-1.64 → cooptools-1.66}/cooptools/finance/__init__.py +0 -0
  45. {cooptools-1.64 → cooptools-1.66}/cooptools/finance/dicounted_cashflow_analysis.py +0 -0
  46. {cooptools-1.64 → cooptools-1.66}/cooptools/finance/fund_projection.py +0 -0
  47. {cooptools-1.64 → cooptools-1.66}/cooptools/finance/fund_projection_renderer.py +0 -0
  48. {cooptools-1.64 → cooptools-1.66}/cooptools/finance/futureValueProjector.py +0 -0
  49. {cooptools-1.64 → cooptools-1.66}/cooptools/finance/growth_projections.py +0 -0
  50. {cooptools-1.64 → cooptools-1.66}/cooptools/finance/reports.py +0 -0
  51. {cooptools-1.64 → cooptools-1.66}/cooptools/finance/utils.py +0 -0
  52. {cooptools-1.64 → cooptools-1.66}/cooptools/geometry_utils/__init__.py +0 -0
  53. {cooptools-1.64 → cooptools-1.66}/cooptools/geometry_utils/circle_utils.py +0 -0
  54. {cooptools-1.64 → cooptools-1.66}/cooptools/geometry_utils/common.py +0 -0
  55. {cooptools-1.64 → cooptools-1.66}/cooptools/geometry_utils/curve_utils.py +0 -0
  56. {cooptools-1.64 → cooptools-1.66}/cooptools/geometry_utils/line_utils.py +0 -0
  57. {cooptools-1.64 → cooptools-1.66}/cooptools/geometry_utils/polygon_utils.py +0 -0
  58. {cooptools-1.64 → cooptools-1.66}/cooptools/geometry_utils/rect_utils.py +0 -0
  59. {cooptools-1.64 → cooptools-1.66}/cooptools/geometry_utils/triangle_utils.py +0 -0
  60. {cooptools-1.64 → cooptools-1.66}/cooptools/geometry_utils/vector_utils.py +0 -0
  61. {cooptools-1.64 → cooptools-1.66}/cooptools/graphs/__init__.py +0 -0
  62. {cooptools-1.64 → cooptools-1.66}/cooptools/graphs/astar_results_viewer.py +0 -0
  63. {cooptools-1.64 → cooptools-1.66}/cooptools/graphs/draw.py +0 -0
  64. {cooptools-1.64 → cooptools-1.66}/cooptools/graphs/graph.py +0 -0
  65. {cooptools-1.64 → cooptools-1.66}/cooptools/graphs/graph_dcs.py +0 -0
  66. {cooptools-1.64 → cooptools-1.66}/cooptools/graphs/graph_definitions.py +0 -0
  67. {cooptools-1.64 → cooptools-1.66}/cooptools/graphs/utils.py +0 -0
  68. {cooptools-1.64 → cooptools-1.66}/cooptools/ideas/__init__.py +0 -0
  69. {cooptools-1.64 → cooptools-1.66}/cooptools/ideas/actionItemTracker/__init__.py +0 -0
  70. {cooptools-1.64 → cooptools-1.66}/cooptools/ideas/actionItemTracker/ai_tracker.py +0 -0
  71. {cooptools-1.64 → cooptools-1.66}/cooptools/ideas/actionItemTracker/dcs.py +0 -0
  72. {cooptools-1.64 → cooptools-1.66}/cooptools/ideas/discreteEventSimulator/__init__.py +0 -0
  73. {cooptools-1.64 → cooptools-1.66}/cooptools/ideas/discreteEventSimulator/discreteEventSimulator.py +0 -0
  74. {cooptools-1.64 → cooptools-1.66}/cooptools/ideas/jsonIO.py +0 -0
  75. {cooptools-1.64 → cooptools-1.66}/cooptools/ideas/metrics.py +0 -0
  76. {cooptools-1.64 → cooptools-1.66}/cooptools/ideas/triggerActionSystem/__init__.py +0 -0
  77. {cooptools-1.64 → cooptools-1.66}/cooptools/ideas/triggerActionSystem/signal.py +0 -0
  78. {cooptools-1.64 → cooptools-1.66}/cooptools/ideas/triggerActionSystem/trigger.py +0 -0
  79. {cooptools-1.64 → cooptools-1.66}/cooptools/loggingHelpers.py +0 -0
  80. {cooptools-1.64 → cooptools-1.66}/cooptools/marchingSquares.py +0 -0
  81. {cooptools-1.64 → cooptools-1.66}/cooptools/materialHandling/__init__.py +0 -0
  82. {cooptools-1.64 → cooptools-1.66}/cooptools/materialHandling/cli.py +0 -0
  83. {cooptools-1.64 → cooptools-1.66}/cooptools/materialHandling/dcs.py +0 -0
  84. {cooptools-1.64 → cooptools-1.66}/cooptools/materialHandling/selectionCriteria.py +0 -0
  85. {cooptools-1.64 → cooptools-1.66}/cooptools/matrixManipulation.py +0 -0
  86. {cooptools-1.64 → cooptools-1.66}/cooptools/os_manip.py +0 -0
  87. {cooptools-1.64 → cooptools-1.66}/cooptools/pandasHelpers.py +0 -0
  88. {cooptools-1.64 → cooptools-1.66}/cooptools/physics/__init__.py +0 -0
  89. {cooptools-1.64 → cooptools-1.66}/cooptools/physics/demo/__init__.py +0 -0
  90. {cooptools-1.64 → cooptools-1.66}/cooptools/physics/demo/server.py +0 -0
  91. {cooptools-1.64 → cooptools-1.66}/cooptools/physics/kinematic.py +0 -0
  92. {cooptools-1.64 → cooptools-1.66}/cooptools/physics/movement.py +0 -0
  93. {cooptools-1.64 → cooptools-1.66}/cooptools/physics/trajectory.py +0 -0
  94. {cooptools-1.64 → cooptools-1.66}/cooptools/physics/waypoint.py +0 -0
  95. {cooptools-1.64 → cooptools-1.66}/cooptools/physics/waypointFollower.py +0 -0
  96. {cooptools-1.64 → cooptools-1.66}/cooptools/plotting.py +0 -0
  97. {cooptools-1.64 → cooptools-1.66}/cooptools/printing.py +0 -0
  98. {cooptools-1.64 → cooptools-1.66}/cooptools/protocols.py +0 -0
  99. {cooptools-1.64 → cooptools-1.66}/cooptools/qualifiers/__init__.py +0 -0
  100. {cooptools-1.64 → cooptools-1.66}/cooptools/qualifiers/cli.py +0 -0
  101. {cooptools-1.64 → cooptools-1.66}/cooptools/qualifiers/qualifier.py +0 -0
  102. {cooptools-1.64 → cooptools-1.66}/cooptools/randoms.py +0 -0
  103. {cooptools-1.64 → cooptools-1.66}/cooptools/register.py +0 -0
  104. {cooptools-1.64 → cooptools-1.66}/cooptools/reservation/__init__.py +0 -0
  105. {cooptools-1.64 → cooptools-1.66}/cooptools/reservation/dcs.py +0 -0
  106. {cooptools-1.64 → cooptools-1.66}/cooptools/reservation/enums.py +0 -0
  107. {cooptools-1.64 → cooptools-1.66}/cooptools/reservation/reservationmanager.py +0 -0
  108. {cooptools-1.64 → cooptools-1.66}/cooptools/retry.py +0 -0
  109. {cooptools-1.64 → cooptools-1.66}/cooptools/sectors/__init__.py +0 -0
  110. {cooptools-1.64 → cooptools-1.66}/cooptools/sectors/grids/__init__.py +0 -0
  111. {cooptools-1.64 → cooptools-1.66}/cooptools/sectors/grids/gridState.py +0 -0
  112. {cooptools-1.64 → cooptools-1.66}/cooptools/sectors/grids/grid_base.py +0 -0
  113. {cooptools-1.64 → cooptools-1.66}/cooptools/sectors/grids/hexGrid.py +0 -0
  114. {cooptools-1.64 → cooptools-1.66}/cooptools/sectors/grids/rectGrid.py +0 -0
  115. {cooptools-1.64 → cooptools-1.66}/cooptools/sectors/hex_utils.py +0 -0
  116. {cooptools-1.64 → cooptools-1.66}/cooptools/sectors/sect_utils.py +0 -0
  117. {cooptools-1.64 → cooptools-1.66}/cooptools/sectors/sectorTree/__init__.py +0 -0
  118. {cooptools-1.64 → cooptools-1.66}/cooptools/sectors/sectorTree/sectorTree.py +0 -0
  119. {cooptools-1.64 → cooptools-1.66}/cooptools/statistics/__init__.py +0 -0
  120. {cooptools-1.64 → cooptools-1.66}/cooptools/statistics/activityReport.py +0 -0
  121. {cooptools-1.64 → cooptools-1.66}/cooptools/statistics/controlChart/__init__.py +0 -0
  122. {cooptools-1.64 → cooptools-1.66}/cooptools/statistics/controlChart/controlChart.py +0 -0
  123. {cooptools-1.64 → cooptools-1.66}/cooptools/statistics/controlChart/plotting.py +0 -0
  124. {cooptools-1.64 → cooptools-1.66}/cooptools/statistics/oee/__init__.py +0 -0
  125. {cooptools-1.64 → cooptools-1.66}/cooptools/statistics/oee/oeeHub.py +0 -0
  126. {cooptools-1.64 → cooptools-1.66}/cooptools/taskProcessing/__init__.py +0 -0
  127. {cooptools-1.64 → cooptools-1.66}/cooptools/taskProcessing/dcs.py +0 -0
  128. {cooptools-1.64 → cooptools-1.66}/cooptools/taskProcessing/taskProcessor.py +0 -0
  129. {cooptools-1.64 → cooptools-1.66}/cooptools/timeTracker/__init__.py +0 -0
  130. {cooptools-1.64 → cooptools-1.66}/cooptools/timeTracker/decay.py +0 -0
  131. {cooptools-1.64 → cooptools-1.66}/cooptools/timeTracker/timeTracker.py +0 -0
  132. {cooptools-1.64 → cooptools-1.66}/cooptools/timeWindow.py +0 -0
  133. {cooptools-1.64 → cooptools-1.66}/cooptools/toggles.py +0 -0
  134. {cooptools-1.64 → cooptools-1.66}/cooptools/transform.py +0 -0
  135. {cooptools-1.64 → cooptools-1.66}/cooptools/trends.py +0 -0
  136. {cooptools-1.64 → cooptools-1.66}/cooptools/typeProviders.py +0 -0
  137. {cooptools-1.64 → cooptools-1.66}/cooptools/typevalidation.py +0 -0
  138. {cooptools-1.64 → cooptools-1.66}/cooptools/version.py +0 -0
  139. {cooptools-1.64 → cooptools-1.66}/cooptools.egg-info/SOURCES.txt +0 -0
  140. {cooptools-1.64 → cooptools-1.66}/cooptools.egg-info/dependency_links.txt +0 -0
  141. {cooptools-1.64 → cooptools-1.66}/cooptools.egg-info/not-zip-safe +0 -0
  142. {cooptools-1.64 → cooptools-1.66}/cooptools.egg-info/requires.txt +0 -0
  143. {cooptools-1.64 → cooptools-1.66}/cooptools.egg-info/top_level.txt +0 -0
  144. {cooptools-1.64 → cooptools-1.66}/requirements.txt +0 -0
  145. {cooptools-1.64 → cooptools-1.66}/setup.cfg +0 -0
  146. {cooptools-1.64 → cooptools-1.66}/tests/test_CliAtomicUserInteraction.py +0 -0
  147. {cooptools-1.64 → cooptools-1.66}/tests/test_asyncable.py +0 -0
  148. {cooptools-1.64 → cooptools-1.66}/tests/test_cdp_bugs.py +0 -0
  149. {cooptools-1.64 → cooptools-1.66}/tests/test_commandDesignPattern.py +0 -0
  150. {cooptools-1.64 → cooptools-1.66}/tests/test_common.py +0 -0
  151. {cooptools-1.64 → cooptools-1.66}/tests/test_config.py +0 -0
  152. {cooptools-1.64 → cooptools-1.66}/tests/test_coopEnum.py +0 -0
  153. {cooptools-1.64 → cooptools-1.66}/tests/test_currency.py +0 -0
  154. {cooptools-1.64 → cooptools-1.66}/tests/test_date_utils.py +0 -0
  155. {cooptools-1.64 → cooptools-1.66}/tests/test_fund_projection.py +0 -0
  156. {cooptools-1.64 → cooptools-1.66}/tests/test_kinematic.py +0 -0
  157. {cooptools-1.64 → cooptools-1.66}/tests/test_os_manip.py +0 -0
  158. {cooptools-1.64 → cooptools-1.66}/tests/test_pandas.py +0 -0
  159. {cooptools-1.64 → cooptools-1.66}/tests/test_printing.py +0 -0
  160. {cooptools-1.64 → cooptools-1.66}/tests/test_qualifiers.py +0 -0
  161. {cooptools-1.64 → cooptools-1.66}/tests/test_sectors.py +0 -0
  162. {cooptools-1.64 → cooptools-1.66}/tests/test_sectors_hex.py +0 -0
  163. {cooptools-1.64 → cooptools-1.66}/tests/test_sectors_sectorTree.py +0 -0
  164. {cooptools-1.64 → cooptools-1.66}/tests/test_statistics.py +0 -0
  165. {cooptools-1.64 → cooptools-1.66}/tests/test_tasks.py +0 -0
  166. {cooptools-1.64 → cooptools-1.66}/tests/test_toggles.py +0 -0
  167. {cooptools-1.64 → cooptools-1.66}/tests/test_trajectory.py +0 -0
  168. {cooptools-1.64 → cooptools-1.66}/tests/test_trends.py +0 -0
  169. {cooptools-1.64 → cooptools-1.66}/tests/test_typevalidation.py +0 -0
  170. {cooptools-1.64 → cooptools-1.66}/tests/test_vector_utils.py +0 -0
  171. {cooptools-1.64 → cooptools-1.66}/tests/test_version.py +0 -0
  172. {cooptools-1.64 → cooptools-1.66}/tests/tests_graph.py +0 -0
  173. {cooptools-1.64 → cooptools-1.66}/tests/tests_gridsystem.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cooptools
3
- Version: 1.64
3
+ Version: 1.66
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)
@@ -182,8 +182,16 @@ def _tick_accel_from_profile(profile: Tuple[PhaseResult, ...], dt: float) -> flo
182
182
  many phases fit within dt so the tick's velocity change matches the true profile
183
183
  instead of overshooting by committing the whole tick to one extreme.
184
184
  """
185
+ if dt < 0:
186
+ raise ValueError(f"_tick_accel_from_profile: dt must be >= 0, got {dt}")
185
187
  if not profile:
186
188
  return 0.0
189
+ if dt == 0:
190
+ # No time elapses this tick (e.g. delta_time_ms=0 for a position-only /
191
+ # realign update) -- the time-weighted average has no window to average
192
+ # over, so fall back to the instantaneous decision: the first phase's
193
+ # acceleration, which is what the weighted average converges to as dt -> 0+.
194
+ return profile[0].accel
187
195
  remaining = dt
188
196
  weighted = 0.0
189
197
  for phase in profile:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cooptools
3
- Version: 1.64
3
+ Version: 1.66
4
4
  Summary: Generic Tooling
5
5
  Home-page: https://github.com/tylertjburns/cooptools
6
6
  Author: tburns
@@ -7,7 +7,7 @@ with open('requirements.txt') as f:
7
7
  requirements = f.read().splitlines()
8
8
 
9
9
  setuptools.setup(name='cooptools',
10
- version='1.64',
10
+ version='1.66',
11
11
  description='Generic Tooling',
12
12
  url='https://github.com/tylertjburns/cooptools',
13
13
  author='tburns',
@@ -71,6 +71,18 @@ class Test_AxisArrivalAccel(unittest.TestCase):
71
71
  self.assertNotEqual(result, a) # not the naive "just keep accelerating" answer
72
72
  self.assertLess(result, a)
73
73
 
74
+ def test__dt_zero__in_braking_regime__does_not_raise_and_returns_brake(self):
75
+ # regression: a position-only update (delta_time_ms=0, e.g. a waypoint
76
+ # realign) while already past the switching threshold used to fall through
77
+ # to single_axis_motion_profile + _tick_accel_from_profile(profile, dt=0),
78
+ # which raised ZeroDivisionError. d=10, v=20, a=5 is the same past-threshold
79
+ # state as test__well_past_stopping_distance__brakes above.
80
+ result = kin._axis_arrival_accel(d=10, v=20, a=5, dt=0)
81
+ self.assertEqual(result, -5.0)
82
+
83
+ def test__dt_zero__in_cheap_accelerate_regime__still_accelerates(self):
84
+ self.assertEqual(kin._axis_arrival_accel(d=1000, v=1, a=5, dt=0), 5.0)
85
+
74
86
 
75
87
  class Test_TickAccelFromProfile(unittest.TestCase):
76
88
  """Direct unit coverage for the tick-blending helper in isolation, using
@@ -100,6 +112,21 @@ class Test_TickAccelFromProfile(unittest.TestCase):
100
112
  result = kin._tick_accel_from_profile((p1,), dt=0.03)
101
113
  self.assertAlmostEqual(result, (-8.0 * 0.01) / 0.03, places=9)
102
114
 
115
+ def test__dt_zero__non_empty_profile__returns_first_phase_accel_without_dividing_by_zero(self):
116
+ # regression: dt=0 (e.g. a position-only / realign update with
117
+ # delta_time_ms=0) used to divide weighted-by-zero into a ZeroDivisionError.
118
+ p1 = kin.PhaseResult(length=0, time=0.01, label="brake", accel=-8.0)
119
+ p2 = kin.PhaseResult(length=0, time=5.0, label="accel", accel=3.0)
120
+ self.assertEqual(kin._tick_accel_from_profile((p1, p2), dt=0), -8.0)
121
+
122
+ def test__dt_zero__empty_profile__returns_zero(self):
123
+ self.assertEqual(kin._tick_accel_from_profile((), dt=0), 0.0)
124
+
125
+ def test__dt_negative__raises_value_error(self):
126
+ p1 = kin.PhaseResult(length=0, time=0.01, label="brake", accel=-8.0)
127
+ with self.assertRaises(ValueError):
128
+ kin._tick_accel_from_profile((p1,), dt=-0.01)
129
+
103
130
 
104
131
  class Test_ArrivalAcceleration(unittest.TestCase):
105
132
  """Direct unit coverage for the multi-axis wrapper around _axis_arrival_accel."""
@@ -285,6 +285,24 @@ class Test_WaypointFollower_RealignToNextWaypoint(unittest.TestCase):
285
285
  self.assertEqual(follower.Position, (2.0, 100.0))
286
286
  self.assertEqual(follower.Velocity.CurrentVelocity_M_S, (1000.0, 0.0))
287
287
 
288
+ def test__update__delta_time_ms_zero__realign_to_next_waypoint__does_not_raise(self):
289
+ # regression: a position-only update (delta_time_ms=0, e.g. a caller placing
290
+ # a mover or re-issuing a route without advancing time) while braking toward
291
+ # a destination used to raise ZeroDivisionError from deep inside
292
+ # arrival_acceleration -> _axis_arrival_accel -> _tick_accel_from_profile,
293
+ # which divided a time-weighted acceleration by dt=0.
294
+ follower = _make_follower()
295
+ follower.add_waypoints([Waypoint(end_pos=(0, 100), is_destination=True)])
296
+ follower._goal_seeker.set_velocity((0, 60)) # fast enough to be past the
297
+ # switching threshold this close
298
+
299
+ # act -- must not raise
300
+ reached = follower.update(delta_time_ms=0, realign_to_next_waypoint=True)
301
+
302
+ # assert -- no time elapsed, so nothing moved and nothing was reached
303
+ self.assertEqual(reached, [])
304
+ self.assertEqual(follower.Position, (0.0, 0.0))
305
+
288
306
 
289
307
  class Test_WaypointFollower_DistanceToNextDestination(unittest.TestCase):
290
308
 
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