| 
 | 1 | +"""  | 
 | 2 | +
  | 
 | 3 | +\eta^3 polynomials planner  | 
 | 4 | +
  | 
 | 5 | +author: Joe Dinius, Ph.D (https://jwdinius.github.io)  | 
 | 6 | +
  | 
 | 7 | +Ref:  | 
 | 8 | +
  | 
 | 9 | +- [\eta^3-Splines for the Smooth Path Generation of Wheeled Mobile Robots](https://ieeexplore.ieee.org/document/4339545/)  | 
 | 10 | +
  | 
 | 11 | +"""  | 
 | 12 | + | 
 | 13 | +import numpy as np  | 
 | 14 | +import matplotlib.pyplot as plt  | 
 | 15 | + | 
 | 16 | +# NOTE: *_pose is a 3-array: 0 - x coord, 1 - y coord, 2 - orientation angle \theta  | 
 | 17 | +class eta3_path(object):  | 
 | 18 | +    """  | 
 | 19 | +    eta3_path  | 
 | 20 | +
  | 
 | 21 | +    input  | 
 | 22 | +        segments: list of `eta3_path_segment` instances definining a continuous path  | 
 | 23 | +    """  | 
 | 24 | +    def __init__(self, segments):  | 
 | 25 | +        # ensure input has the correct form  | 
 | 26 | +        assert(isinstance(segments, list) and isinstance(segments[0], eta3_path_segment))  | 
 | 27 | +        # ensure that each segment begins from the previous segment's end (continuity)  | 
 | 28 | +        for r,s in zip(segments[:-1], segments[1:]):  | 
 | 29 | +            assert(np.array_equal(r.end_pose, s.start_pose))  | 
 | 30 | +        self.segments = segments  | 
 | 31 | +    """  | 
 | 32 | +    eta3_path::calc_path_point  | 
 | 33 | +
  | 
 | 34 | +    input  | 
 | 35 | +        normalized interpolation point along path object, 0 <= u <= len(self.segments)  | 
 | 36 | +    returns  | 
 | 37 | +        2d (x,y) position vector  | 
 | 38 | +    """  | 
 | 39 | +    def calc_path_point(self, u):  | 
 | 40 | +        assert(u >= 0 and u <= len(self.segments))  | 
 | 41 | +        if np.isclose(u, len(self.segments)):  | 
 | 42 | +            segment_idx = len(self.segments)-1  | 
 | 43 | +            u = 1.  | 
 | 44 | +        else:  | 
 | 45 | +            segment_idx = int(np.floor(u))  | 
 | 46 | +            u -= segment_idx  | 
 | 47 | +        return self.segments[segment_idx].calc_point(u)  | 
 | 48 | + | 
 | 49 | + | 
 | 50 | +class eta3_path_segment(object):  | 
 | 51 | +    """  | 
 | 52 | +    eta3_path_segment - constructs an eta^3 path segment based on desired shaping, eta, and curvature vector, kappa.  | 
 | 53 | +                        If either, or both, of eta and kappa are not set during initialization, they will  | 
 | 54 | +                        default to zeros.  | 
 | 55 | +
  | 
 | 56 | +    input  | 
 | 57 | +        start_pose - starting pose array  (x, y, \theta)  | 
 | 58 | +        end_pose - ending pose array (x, y, \theta)  | 
 | 59 | +        eta - shaping parameters, default=None  | 
 | 60 | +        kappa - curvature parameters, default=None  | 
 | 61 | +    """  | 
 | 62 | +    def __init__(self, start_pose, end_pose, eta=None, kappa=None):  | 
 | 63 | +        # make sure inputs are of the correct size  | 
 | 64 | +        assert(len(start_pose) == 3 and len(start_pose) == len(end_pose))  | 
 | 65 | +        self.start_pose = start_pose  | 
 | 66 | +        self.end_pose   = end_pose  | 
 | 67 | +        # if no eta is passed, initialize it to array of zeros  | 
 | 68 | +        if not eta:  | 
 | 69 | +            eta = np.zeros((6,))  | 
 | 70 | +        else:  | 
 | 71 | +            # make sure that eta has correct size  | 
 | 72 | +            assert(len(eta) == 6)  | 
 | 73 | +        # if no kappa is passed, initialize to array of zeros  | 
 | 74 | +        if not kappa:  | 
 | 75 | +            kappa = np.zeros((4,))  | 
 | 76 | +        else:  | 
 | 77 | +            assert(len(kappa) == 4)  | 
 | 78 | +        # set up angle cosines and sines for simpler computations below  | 
 | 79 | +        ca = np.cos(start_pose[2])  | 
 | 80 | +        sa = np.sin(start_pose[2])  | 
 | 81 | +        cb = np.cos(end_pose[2])  | 
 | 82 | +        sb = np.sin(end_pose[2])  | 
 | 83 | +        # 2 dimensions (x,y) x 8 coefficients per dimension  | 
 | 84 | +        self.coeffs = np.empty((2, 8))  | 
 | 85 | +        # constant terms (u^0)  | 
 | 86 | +        self.coeffs[0, 0] = start_pose[0]  | 
 | 87 | +        self.coeffs[1, 0] = start_pose[1]  | 
 | 88 | +        # linear (u^1)  | 
 | 89 | +        self.coeffs[0, 1] = eta[0] * ca  | 
 | 90 | +        self.coeffs[1, 1] = eta[0] * sa  | 
 | 91 | +        # quadratic (u^2)  | 
 | 92 | +        self.coeffs[0, 2] = 1./2 * eta[2] * ca - 1./2 * eta[0]**2 * kappa[0] * sa  | 
 | 93 | +        self.coeffs[1, 2] = 1./2 * eta[2] * sa + 1./2 * eta[0]**2 * kappa[0] * ca  | 
 | 94 | +        # cubic (u^3)  | 
 | 95 | +        self.coeffs[0, 3] = 1./6 * eta[4] * ca - 1./6 * (eta[0]**3 * kappa[1] + 3. * eta[0] * eta[2] * kappa[0]) * sa  | 
 | 96 | +        self.coeffs[1, 3] = 1./6 * eta[4] * sa + 1./6 * (eta[0]**3 * kappa[1] + 3. * eta[0] * eta[2] * kappa[0]) * ca  | 
 | 97 | +        # quartic (u^4)  | 
 | 98 | +        self.coeffs[0, 4] = 35. * (end_pose[0] - start_pose[0]) - (20. * eta[0] + 5 * eta[2] + 2./3 * eta[4]) * ca \  | 
 | 99 | +            + (5. * eta[0]**2 * kappa[0] + 2./3 * eta[0]**3 * kappa[1] + 2. * eta[0] * eta[2] * kappa[0]) * sa \  | 
 | 100 | +            - (15. * eta[1] - 5./2 * eta[3] + 1./6 * eta[5]) * cb \  | 
 | 101 | +            - (5./2 * eta[1]**2 * kappa[2] - 1./6 * eta[1]**3 * kappa[3] - 1./2 * eta[1] * eta[3] * kappa[2]) * sb  | 
 | 102 | +        self.coeffs[1, 4] = 35. * (end_pose[1] - start_pose[1]) - (20. * eta[0] + 5. * eta[2] + 2./3 * eta[4]) * sa \  | 
 | 103 | +            - (5. * eta[0]**2 * kappa[0] + 2./3 * eta[0]**3 * kappa[1] + 2. * eta[0] * eta[2] * kappa[0]) * ca \  | 
 | 104 | +            - (15. * eta[1] - 5./2 * eta[3] + 1./6 * eta[5]) * sb \  | 
 | 105 | +            + (5./2 * eta[1]**2 * kappa[2] - 1./6 * eta[1]**3 * kappa[3] - 1./2 * eta[1] * eta[3] * kappa[2]) * cb  | 
 | 106 | +        # quintic (u^5)  | 
 | 107 | +        self.coeffs[0, 5] = -84. * (end_pose[0] - start_pose[0]) + (45. * eta[0] + 10. * eta[2] + eta[4]) * ca \  | 
 | 108 | +            - (10. * eta[0]**2 * kappa[0] + eta[0]**3 * kappa[1] + 3. * eta[0] * eta[2] * kappa[0]) * sa \  | 
 | 109 | +            + (39. * eta[1] - 7. * eta[3] + 1./2 * eta[5]) * cb \  | 
 | 110 | +            + (7. * eta[1]**2 * kappa[2] - 1./2 * eta[1]**3 * kappa[3] - 3./2 * eta[1] * eta[3] * kappa[2]) * sb  | 
 | 111 | +        self.coeffs[1, 5] = -84. * (end_pose[1] - start_pose[1]) + (45. * eta[0] + 10. * eta[2] + eta[4]) * sa \  | 
 | 112 | +            + (10. * eta[0]**2 * kappa[0] + eta[0]**3 * kappa[1] + 3. * eta[0] * eta[2] * kappa[0]) * ca \  | 
 | 113 | +            + (39. * eta[1] - 7. * eta[3] + 1./2 * eta[5]) * sb \  | 
 | 114 | +            - (7. * eta[1]**2 * kappa[2] - 1./2 * eta[1]**3 * kappa[3] - 3./2 * eta[1] * eta[3] * kappa[2]) * cb  | 
 | 115 | +        # sextic (u^6)  | 
 | 116 | +        self.coeffs[0, 6] = 70. * (end_pose[0] - start_pose[0]) - (36. * eta[0] + 15./2 * eta[2] + 2./3 * eta[4]) * ca \  | 
 | 117 | +            + (15./2 * eta[0]**2 * kappa[0] + 2./3 * eta[0]**3 * kappa[1] + 2. * eta[0] * eta[2] * kappa[0]) * sa \  | 
 | 118 | +            - (34. * eta[1] - 13./2 * eta[3] + 1./2 * eta[5]) * cb \  | 
 | 119 | +            - (13./2 * eta[1]**2 * kappa[2] - 1./2 * eta[1]**3 * kappa[3] - 3./2 * eta[1] * eta[3] * kappa[2]) * sb  | 
 | 120 | +        self.coeffs[1, 6] = 70. * (end_pose[1] - start_pose[1]) - (36. * eta[0] + 15./2 * eta[2] + 2./3 * eta[4]) * sa \  | 
 | 121 | +            - (15./2 * eta[0]**2 * kappa[0] + 2./3 * eta[0]**3 * kappa[1] + 2. * eta[0] * eta[2] * kappa[0]) * ca \  | 
 | 122 | +            - (34. * eta[1] - 13./2 * eta[3] + 1./2 * eta[5]) * sb \  | 
 | 123 | +            + (13./2 * eta[1]**2 * kappa[2] - 1./2 * eta[1]**3 * kappa[3] - 3./2 * eta[1] * eta[3] * kappa[2]) * cb  | 
 | 124 | +        # septic (u^7)  | 
 | 125 | +        self.coeffs[0, 7] = -20. * (end_pose[0] - start_pose[0]) + (10. * eta[0] + 2. * eta[2] + 1./6 * eta[4]) * ca \  | 
 | 126 | +            - (2. * eta[0]**2 * kappa[0] + 1./6 * eta[0]**3 * kappa[1] + 1./2 * eta[0] * eta[2] * kappa[0]) * sa \  | 
 | 127 | +            + (10. * eta[1] - 2. * eta[3] + 1./6 * eta[5]) * cb \  | 
 | 128 | +            + (2. * eta[1]**2 * kappa[2] - 1./6 * eta[1]**3 * kappa[3] - 1./2 * eta[1] * eta[3] * kappa[2]) * sb  | 
 | 129 | +        self.coeffs[1, 7] = -20. * (end_pose[1] - start_pose[1]) + (10. * eta[0] + 2. * eta[2] + 1./6 * eta[4]) * sa \  | 
 | 130 | +            + (2. * eta[0]**2 * kappa[0] + 1./6 * eta[0]**3 * kappa[1] + 1./2 * eta[0] * eta[2] * kappa[0]) * ca \  | 
 | 131 | +            + (10. * eta[1] - 2. * eta[3] + 1./6 * eta[5]) * sb \  | 
 | 132 | +            - (2. * eta[1]**2 * kappa[2] - 1./6 * eta[1]**3 * kappa[3] - 1./2 * eta[1] * eta[3] * kappa[2]) * cb  | 
 | 133 | +    """  | 
 | 134 | +    eta3_path_segment::calc_point  | 
 | 135 | +      | 
 | 136 | +    input  | 
 | 137 | +        u - parametric representation of a point along the segment, 0 <= u <= 1  | 
 | 138 | +    returns  | 
 | 139 | +        (x,y) of point along the segment  | 
 | 140 | +    """  | 
 | 141 | +    def calc_point(self, u):  | 
 | 142 | +        assert(u >= 0 and u <= 1)  | 
 | 143 | +        return self.coeffs.dot(np.array([1, u, u**2, u**3, u**4, u**5, u**6, u**7]))  | 
 | 144 | + | 
 | 145 | + | 
 | 146 | +def main():  | 
 | 147 | +    """  | 
 | 148 | +    recreate path from reference (see Table 1)  | 
 | 149 | +    """  | 
 | 150 | +    path_segments = []  | 
 | 151 | +      | 
 | 152 | +    # segment 1: lane-change curve  | 
 | 153 | +    start_pose = [0, 0, 0]  | 
 | 154 | +    end_pose   = [4, 1.5, 0]  | 
 | 155 | +    # NOTE: The ordering on kappa is [kappa_A, kappad_A, kappa_B, kappad_B], with kappad_* being the curvature derivative  | 
 | 156 | +    kappa      = [0, 0, 0, 0]  | 
 | 157 | +    eta        = [4.27, 4.27, 0, 0, 0, 0]  | 
 | 158 | +    path_segments.append(eta3_path_segment(start_pose=start_pose, end_pose=end_pose, eta=eta, kappa=kappa))  | 
 | 159 | +      | 
 | 160 | +    # segment 2: line segment  | 
 | 161 | +    start_pose = [4, 1.5, 0]  | 
 | 162 | +    end_pose   = [5.5, 1.5, 0]  | 
 | 163 | +    kappa      = [0, 0, 0, 0]  | 
 | 164 | +    eta        = [0, 0, 0, 0, 0, 0]  | 
 | 165 | +    path_segments.append(eta3_path_segment(start_pose=start_pose, end_pose=end_pose, eta=eta, kappa=kappa))  | 
 | 166 | + | 
 | 167 | +    # segment 3: cubic spiral  | 
 | 168 | +    start_pose = [5.5, 1.5, 0]  | 
 | 169 | +    end_pose   = [7.4377, 1.8235, 0.6667]  | 
 | 170 | +    kappa      = [0, 0, 1, 1]  | 
 | 171 | +    eta        = [1.88, 1.88, 0, 0, 0, 0]  | 
 | 172 | +    path_segments.append(eta3_path_segment(start_pose=start_pose, end_pose=end_pose, eta=eta, kappa=kappa))  | 
 | 173 | + | 
 | 174 | +    # segment 4: generic twirl arc  | 
 | 175 | +    start_pose = [7.4377, 1.8235, 0.6667]  | 
 | 176 | +    end_pose   = [7.8, 4.3, 1.8]  | 
 | 177 | +    kappa      = [1, 1, 0.5, 0]  | 
 | 178 | +    eta        = [7, 10, 10, -10, 4, 4]  | 
 | 179 | +    path_segments.append(eta3_path_segment(start_pose=start_pose, end_pose=end_pose, eta=eta, kappa=kappa))  | 
 | 180 | + | 
 | 181 | +    # segment 5: circular arc  | 
 | 182 | +    start_pose = [7.8, 4.3, 1.8]  | 
 | 183 | +    end_pose   = [5.4581, 5.8064, 3.3416]  | 
 | 184 | +    kappa      = [0.5, 0, 0.5, 0]  | 
 | 185 | +    eta        = [2.98, 2.98, 0, 0, 0, 0]  | 
 | 186 | +    path_segments.append(eta3_path_segment(start_pose=start_pose, end_pose=end_pose, eta=eta, kappa=kappa))  | 
 | 187 | + | 
 | 188 | +    # construct the whole path  | 
 | 189 | +    path = eta3_path(path_segments)  | 
 | 190 | + | 
 | 191 | +    # interpolate at several points along the path  | 
 | 192 | +    ui = np.linspace(0, len(path_segments), 1001)  | 
 | 193 | +    pos = np.empty((2, ui.size))  | 
 | 194 | +    for i,u in enumerate(ui):  | 
 | 195 | +        pos[:, i] = path.calc_path_point(u)  | 
 | 196 | + | 
 | 197 | +    # plot the path  | 
 | 198 | +    plt.figure('Path from Reference')  | 
 | 199 | +    plt.plot(pos[0, :], pos[1, :])  | 
 | 200 | +    plt.xlabel('x')  | 
 | 201 | +    plt.ylabel('y')  | 
 | 202 | +    plt.title('Path')  | 
 | 203 | +    plt.show()  | 
 | 204 | + | 
 | 205 | +if __name__ == '__main__':  | 
 | 206 | +    main()  | 
0 commit comments