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.
- {cooptools-1.63 → cooptools-1.65}/PKG-INFO +1 -1
- {cooptools-1.63 → cooptools-1.65}/cooptools/decor.py +39 -7
- {cooptools-1.63 → cooptools-1.65}/cooptools/geometry_utils/circle_utils.py +158 -2
- {cooptools-1.63 → cooptools-1.65}/cooptools/graphs/graph.py +31 -16
- cooptools-1.65/cooptools/physics/demo/server.py +136 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/physics/kinematic.py +88 -10
- cooptools-1.65/cooptools/physics/kinematic_utils.py +331 -0
- cooptools-1.65/cooptools/physics/trajectory.py +718 -0
- cooptools-1.65/cooptools/physics/waypointFollower.py +320 -0
- cooptools-1.65/cooptools/statistics/oee/__init__.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools.egg-info/PKG-INFO +1 -1
- {cooptools-1.63 → cooptools-1.65}/cooptools.egg-info/SOURCES.txt +6 -0
- {cooptools-1.63 → cooptools-1.65}/setup.py +1 -1
- cooptools-1.65/tests/test_kinematic.py +174 -0
- cooptools-1.65/tests/test_kinematic_utils.py +310 -0
- cooptools-1.65/tests/test_trajectory.py +562 -0
- cooptools-1.65/tests/test_waypointFollower.py +470 -0
- cooptools-1.63/cooptools/physics/kinematic_utils.py +0 -382
- cooptools-1.63/cooptools/physics/trajectory.py +0 -343
- cooptools-1.63/cooptools/physics/waypointFollower.py +0 -206
- {cooptools-1.63 → cooptools-1.65}/MANIFEST.in +0 -0
- {cooptools-1.63 → cooptools-1.65}/README.md +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/__init__.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/anchor.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/asyncable.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/cli/CliAtomicUserInteraction.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/cli/CliMenu.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/cli/__init__.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/cli/fileContentReturn.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/cnxn_info.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/colors.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/commandDesignPattern/__init__.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/commandDesignPattern/commandController.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/commandDesignPattern/commandProtocol.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/commandDesignPattern/exceptions.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/common.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/config.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/coopDataclass.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/coopEnum.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/coopthreading.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/currency.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/dataRefresher/__init__.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/dataRefresher/dataHub.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/dataRefresher/dataRefresher.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/dataStore/__init__.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/dataStore/dataProcessor.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/dataStore/dataStoreProtocol.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/dataStore/dbConnectionURI.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/dataStore/inMemoryDataStore.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/date_utils.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/decay.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/dictPolicies.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/exceptions.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/expertise/__init__.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/expertise/expertiseArgs.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/expertise/expertiseSchedules.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/finance/__init__.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/finance/dicounted_cashflow_analysis.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/finance/fund_projection.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/finance/fund_projection_renderer.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/finance/futureValueProjector.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/finance/growth_projections.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/finance/reports.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/finance/utils.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/geometry_utils/__init__.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/geometry_utils/common.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/geometry_utils/curve_utils.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/geometry_utils/line_utils.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/geometry_utils/polygon_utils.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/geometry_utils/rect_utils.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/geometry_utils/triangle_utils.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/geometry_utils/vector_utils.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/graphs/__init__.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/graphs/astar_results_viewer.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/graphs/draw.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/graphs/graph_dcs.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/graphs/graph_definitions.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/graphs/utils.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/ideas/__init__.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/ideas/actionItemTracker/__init__.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/ideas/actionItemTracker/ai_tracker.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/ideas/actionItemTracker/dcs.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/ideas/discreteEventSimulator/__init__.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/ideas/discreteEventSimulator/discreteEventSimulator.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/ideas/jsonIO.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/ideas/metrics.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/ideas/triggerActionSystem/__init__.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/ideas/triggerActionSystem/signal.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/ideas/triggerActionSystem/trigger.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/loggingHelpers.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/marchingSquares.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/materialHandling/__init__.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/materialHandling/cli.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/materialHandling/dcs.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/materialHandling/selectionCriteria.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/matrixManipulation.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/os_manip.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/pandasHelpers.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/physics/__init__.py +0 -0
- {cooptools-1.63/cooptools/reservation → cooptools-1.65/cooptools/physics/demo}/__init__.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/physics/movement.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/physics/waypoint.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/plotting.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/printing.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/protocols.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/qualifiers/__init__.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/qualifiers/cli.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/qualifiers/qualifier.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/randoms.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/register.py +0 -0
- {cooptools-1.63/cooptools/statistics/controlChart → cooptools-1.65/cooptools/reservation}/__init__.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/reservation/dcs.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/reservation/enums.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/reservation/reservationmanager.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/retry.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/sectors/__init__.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/sectors/grids/__init__.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/sectors/grids/gridState.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/sectors/grids/grid_base.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/sectors/grids/hexGrid.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/sectors/grids/rectGrid.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/sectors/hex_utils.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/sectors/sect_utils.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/sectors/sectorTree/__init__.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/sectors/sectorTree/sectorTree.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/statistics/__init__.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/statistics/activityReport.py +0 -0
- {cooptools-1.63/cooptools/statistics/oee → cooptools-1.65/cooptools/statistics/controlChart}/__init__.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/statistics/controlChart/controlChart.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/statistics/controlChart/plotting.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/statistics/oee/oeeHub.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/taskProcessing/__init__.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/taskProcessing/dcs.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/taskProcessing/taskProcessor.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/timeTracker/__init__.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/timeTracker/decay.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/timeTracker/timeTracker.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/timeWindow.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/toggles.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/transform.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/trends.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/typeProviders.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/typevalidation.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools/version.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools.egg-info/dependency_links.txt +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools.egg-info/not-zip-safe +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools.egg-info/requires.txt +0 -0
- {cooptools-1.63 → cooptools-1.65}/cooptools.egg-info/top_level.txt +0 -0
- {cooptools-1.63 → cooptools-1.65}/requirements.txt +0 -0
- {cooptools-1.63 → cooptools-1.65}/setup.cfg +0 -0
- {cooptools-1.63 → cooptools-1.65}/tests/test_CliAtomicUserInteraction.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/tests/test_asyncable.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/tests/test_cdp_bugs.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/tests/test_commandDesignPattern.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/tests/test_common.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/tests/test_config.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/tests/test_coopEnum.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/tests/test_currency.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/tests/test_date_utils.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/tests/test_fund_projection.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/tests/test_os_manip.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/tests/test_pandas.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/tests/test_printing.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/tests/test_qualifiers.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/tests/test_sectors.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/tests/test_sectors_hex.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/tests/test_sectors_sectorTree.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/tests/test_statistics.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/tests/test_tasks.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/tests/test_toggles.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/tests/test_trends.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/tests/test_typevalidation.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/tests/test_vector_utils.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/tests/test_version.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/tests/tests_graph.py +0 -0
- {cooptools-1.63 → cooptools-1.65}/tests/tests_gridsystem.py +0 -0
|
@@ -2,7 +2,8 @@ import functools
|
|
|
2
2
|
import time
|
|
3
3
|
import logging
|
|
4
4
|
import inspect
|
|
5
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
688
|
-
|
|
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
|
-
|
|
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
|
|
710
|
+
while open_heap:
|
|
704
711
|
cc += 1
|
|
705
712
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
'''
|
|
226
|
-
|
|
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] <
|
|
231
|
-
abs(vector_to_goal[ii]) <
|
|
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=
|
|
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.
|
|
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
|