|
1 | 1 | # ============================================================================== |
2 | 2 | # File Name: S7RTT.py |
3 | 3 | # Author: feecat |
4 | | -# Version: V1.2 |
| 4 | +# Version: V1.3 |
5 | 5 | # Description: Simple 7seg Real-Time Trajectory Generator |
6 | 6 | # Website: https://github.com/feecat/S7RTT |
7 | 7 | # License: Apache License Version 2.0 |
|
24 | 24 |
|
25 | 25 | class Solver: |
26 | 26 | """ |
27 | | - A numerical solver utility using Brent's Method. |
28 | | - Primarily used to find the exact peak velocity required to align the |
29 | | - trajectory's total distance with the target position. |
| 27 | + Optimized Numerical Solver using Brent's Method. |
30 | 28 | """ |
31 | 29 |
|
32 | | - EPS_TOL = 1e-8 # Tolerance for function value convergence |
33 | | - EPS_CONVERGE = 1e-12 # Tolerance for interval convergence |
34 | | - MAX_ITER = 50 # Maximum iterations to prevent infinite loops |
| 30 | + # Use system machine epsilon (typically around 2.22e-16) for better accuracy |
| 31 | + EPS = 1e-16 |
| 32 | + TOL = 1e-12 |
| 33 | + MAX_ITER = 100 |
35 | 34 |
|
36 | 35 | @staticmethod |
37 | | - def solve_monotonic_brent(func, low, high): |
| 36 | + def solve_brent(func, low, high): |
38 | 37 | """ |
39 | | - Finds the root of a monotonic function 'func' within the interval [low, high]. |
40 | | - """ |
41 | | - f_low = func(low) |
42 | | - f_high = func(high) |
| 38 | + Finds the root of 'func' in [low, high] using Brent's method. |
43 | 39 | |
44 | | - # If the root is bracketed, return the side closer to zero |
45 | | - if f_low * f_high > 0: |
46 | | - if abs(f_low) < abs(f_high): return low |
47 | | - else: return high |
48 | | - |
49 | | - a = low; b = high |
50 | | - fa = f_low; fb = f_high |
| 40 | + Returns: |
| 41 | + The root x (or the closest approximation if not converged). |
| 42 | + """ |
| 43 | + a = low |
| 44 | + b = high |
| 45 | + fa = func(a) |
| 46 | + fb = func(b) |
| 47 | + |
| 48 | + # Check if the root is bracketed. |
| 49 | + # If fa and fb have the same sign, we cannot guarantee a root exists. |
| 50 | + # In this case, return the endpoint closer to zero. |
| 51 | + if (fa > 0 and fb > 0) or (fa < 0 and fb < 0): |
| 52 | + return a if abs(fa) < abs(fb) else b |
| 53 | + |
| 54 | + if fa == 0: return a |
| 55 | + if fb == 0: return b |
| 56 | + |
| 57 | + # Initialize variables |
| 58 | + # c is the opposite end of the interval from b |
| 59 | + c = a |
| 60 | + fc = fa |
| 61 | + d = e = b - a |
51 | 62 |
|
52 | | - root_est = b |
| 63 | + # mflag indicates if bisection is forced |
| 64 | + mflag = True |
53 | 65 |
|
| 66 | + tol_const = Solver.TOL |
| 67 | + mach_eps = Solver.EPS |
| 68 | + |
54 | 69 | for _ in range(Solver.MAX_ITER): |
55 | | - # Ensure 'b' is the better estimate (closer to 0) |
56 | | - if abs(fa) < abs(fb): |
57 | | - a, b = b, a |
58 | | - fa, fb = fb, fa |
59 | | - |
60 | | - # Check for convergence |
61 | | - if abs(fb - fa) < Solver.EPS_CONVERGE: |
62 | | - root_est = b |
63 | | - break |
64 | | - else: |
65 | | - # Try Inverse Quadratic Interpolation |
66 | | - if abs(fa - fb) > 1e-14: |
67 | | - root_est = b - fb * (b - a) / (fb - fa) |
68 | | - else: |
69 | | - root_est = b |
70 | | - |
71 | | - mid_val = 0.5 * (a + b) |
| 70 | + # Ensure |f(b)| <= |f(c)| so that b is always the best guess |
| 71 | + if abs(fc) < abs(fb): |
| 72 | + a, b, c = b, c, b |
| 73 | + fa, fb, fc = fb, fc, fb |
72 | 74 |
|
73 | | - # Conditions to fall back to Bisection method |
74 | | - cond1 = (root_est < mid_val and b < mid_val) |
75 | | - cond2 = (root_est > mid_val and b > mid_val) |
| 75 | + # Calculate tolerance |
| 76 | + # tol1 combines relative machine precision and absolute tolerance |
| 77 | + tol1 = 2.0 * mach_eps * abs(b) + 0.5 * tol_const |
| 78 | + xm = 0.5 * (c - b) |
76 | 79 |
|
77 | | - if cond1 or cond2: |
78 | | - root_est = mid_val |
| 80 | + # Convergence test: interval is small enough or exact root found |
| 81 | + if abs(xm) <= tol1 or fb == 0: |
| 82 | + return b |
79 | 83 |
|
80 | | - # Ensure estimate stays within bounds |
81 | | - min_val = a if a < b else b |
82 | | - max_val = a if a > b else b |
83 | | - |
84 | | - if root_est < min_val or root_est > max_val: |
85 | | - root_est = mid_val |
| 84 | + # Attempt interpolation if: |
| 85 | + # 1. Previous step size |e| was larger than tolerance |
| 86 | + # 2. Function value at 'a' is significantly different from 'b' |
| 87 | + if abs(e) >= tol1 and abs(fa) > abs(fb): |
| 88 | + s = fb / fa |
86 | 89 |
|
87 | | - f_root = func(root_est) |
88 | | - |
89 | | - # Check if result is within tolerance |
90 | | - if abs(f_root) < Solver.EPS_TOL or abs(b - a) < Solver.EPS_TOL: |
91 | | - return root_est |
| 90 | + if a == c: |
| 91 | + # Linear interpolation (Secant Method) |
| 92 | + p = 2.0 * xm * s |
| 93 | + q = 1.0 - s |
| 94 | + else: |
| 95 | + # Inverse Quadratic Interpolation (IQI) |
| 96 | + q = fa / fc |
| 97 | + r = fb / fc |
| 98 | + p = s * (2.0 * xm * q * (q - r) - (b - a) * (r - 1.0)) |
| 99 | + q = (q - 1.0) * (r - 1.0) * (s - 1.0) |
| 100 | + |
| 101 | + # Adjust signs of p and q ensuring p > 0 |
| 102 | + if p > 0: |
| 103 | + q = -q |
| 104 | + p = abs(p) |
| 105 | + |
| 106 | + # Validate if interpolation result is acceptable: |
| 107 | + # 1. Must fall strictly within (b, b + 3*xm) |
| 108 | + # 2. Must converge faster than bisection (checked against min2) |
| 109 | + min1 = 3.0 * xm * q - abs(tol1 * q) |
| 110 | + min2 = abs(e * q) |
| 111 | + |
| 112 | + if 2.0 * p < min1: |
| 113 | + # Check convergence speed relative to previous step |
| 114 | + if mflag: |
| 115 | + # Previous step was bisection; require significant reduction |
| 116 | + if 2.0 * p < min2: |
| 117 | + e = d |
| 118 | + d = p / q |
| 119 | + mflag = False |
| 120 | + else: |
| 121 | + d = xm; e = d; mflag = True |
| 122 | + else: |
| 123 | + # Previous step was interpolation; check against d form step before last |
| 124 | + if 2.0 * p < abs(d * q): |
| 125 | + e = d |
| 126 | + d = p / q |
| 127 | + mflag = False |
| 128 | + else: |
| 129 | + d = xm; e = d; mflag = True |
| 130 | + else: |
| 131 | + # Interpolation failed (out of bounds), fallback to bisection |
| 132 | + d = xm; e = d; mflag = True |
| 133 | + else: |
| 134 | + # Step too small or function too flat -> force bisection |
| 135 | + d = xm; e = d; mflag = True |
| 136 | + |
| 137 | + # Apply the computed step |
| 138 | + a = b; fa = fb # Store previous b as a (for the next bracket) |
92 | 139 |
|
93 | | - # Update the brackets based on the sign |
94 | | - if fa * f_root < 0: |
95 | | - b = root_est; fb = f_root |
| 140 | + if abs(d) > tol1: |
| 141 | + b += d |
96 | 142 | else: |
97 | | - a = root_est; fa = f_root |
| 143 | + # If step is too small, force a tiny step to avoid stagnation |
| 144 | + b += math.copysign(tol1, xm) |
98 | 145 |
|
99 | | - return root_est |
| 146 | + fb = func(b) |
| 147 | + |
| 148 | + # Maintain bracket [b, c] such that f(b) and f(c) have opposite signs. |
| 149 | + # If f(b) and f(c) share the same sign, the root is no longer between them. |
| 150 | + # Reset c to a (the previous b). |
| 151 | + if (fb > 0 and fc > 0) or (fb < 0 and fc < 0): |
| 152 | + c = a |
| 153 | + fc = fa |
| 154 | + d = e = b - a # Reset step size |
| 155 | + mflag = True |
| 156 | + |
| 157 | + # If max iterations reached, return the best guess b |
| 158 | + return b |
100 | 159 |
|
101 | 160 |
|
102 | 161 | class MotionState: |
@@ -358,14 +417,14 @@ def get_error(v_p): |
358 | 417 |
|
359 | 418 | if s_low >= s_high: s_low = s_high - S7RTT.EPS_VAL |
360 | 419 |
|
361 | | - best_v = Solver.solve_monotonic_brent(get_error, s_low, s_high) |
| 420 | + best_v = Solver.solve_brent(get_error, s_low, s_high) |
362 | 421 |
|
363 | 422 | # Strategy C: Fallback Mechanism |
364 | 423 | hit_low = abs(best_v - s_low) < S7RTT.EPS_VAL |
365 | 424 | hit_high = abs(best_v - s_high) < S7RTT.EPS_VAL |
366 | 425 |
|
367 | 426 | if hit_low or hit_high: |
368 | | - best_v = Solver.solve_monotonic_brent(get_error, -limit_safe, limit_safe) |
| 427 | + best_v = Solver.solve_brent(get_error, -limit_safe, limit_safe) |
369 | 428 |
|
370 | 429 | # Post-Process: Micro-noise filtering |
371 | 430 | # If result implies v_inertial, use the cached shapes to save calculation |
|
0 commit comments