|
| 1 | +from roboticstoolbox import ETS |
| 2 | +import re |
| 3 | +import sympy |
| 4 | +import numpy as np |
| 5 | +from spatialmath import base |
| 6 | + |
| 7 | +pi2 = base.pi() / 2 |
| 8 | +deg = base.pi() / sympy.Integer('180') |
| 9 | + |
| 10 | +class DHFactor(ETS): |
| 11 | + |
| 12 | + def __init__(self, s): |
| 13 | + et_re = re.compile(r"([RT][xyz])\(([^)]*)\)") |
| 14 | + |
| 15 | + super().__init__() |
| 16 | + # self.data = [] |
| 17 | + |
| 18 | + for axis, eta in et_re.findall(s): |
| 19 | + print(axis,eta) |
| 20 | + if eta[0] == 'q': |
| 21 | + eta = None |
| 22 | + unit = None |
| 23 | + else: |
| 24 | + # eta can be given as a variable or a number |
| 25 | + try: |
| 26 | + # first attempt to create symbolic number |
| 27 | + eta = sympy.Number(eta) |
| 28 | + except: |
| 29 | + # failing that, a symbolic variable |
| 30 | + eta = sympy.symbols(eta) |
| 31 | + if axis[0] == 'R': |
| 32 | + # convert to degrees, assumed unit in input string |
| 33 | + eta = sympy.simplify(eta * deg) |
| 34 | + |
| 35 | + if axis == 'Rx': |
| 36 | + e = ETS.rx(eta) |
| 37 | + elif axis == 'Ry': |
| 38 | + e = ETS.ry(eta) |
| 39 | + elif axis == 'Rz': |
| 40 | + e = ETS.rz(eta) |
| 41 | + elif axis == 'Tx': |
| 42 | + e = ETS.tx(eta) |
| 43 | + elif axis == 'Ty': |
| 44 | + e = ETS.ty(eta) |
| 45 | + elif axis == 'Tz': |
| 46 | + e = ETS.tz(eta) |
| 47 | + |
| 48 | + self.data.append(e.data[0]) |
| 49 | + |
| 50 | + def simplify(self): |
| 51 | + |
| 52 | + self.merge() |
| 53 | + self.swap(convention) |
| 54 | + self.merge() |
| 55 | + self.float_right() |
| 56 | + self.merge() |
| 57 | + self.substitute_to_z() |
| 58 | + self.merge() |
| 59 | + for i in range(nloops): |
| 60 | + nchanges = 0 |
| 61 | + |
| 62 | + nchanges += self.merge() |
| 63 | + nchanges += self.swap(convention) |
| 64 | + nchanges += self.merge() |
| 65 | + nchanges += self.eliminate_y() |
| 66 | + nchanges += self.merge() |
| 67 | + |
| 68 | + if nchanges == 0: |
| 69 | + # try this then |
| 70 | + nchanges += self.substitute_to_z2() |
| 71 | + if nchanges == 0: |
| 72 | + break |
| 73 | + else: |
| 74 | + # too many iterations |
| 75 | + print('too many iterations') |
| 76 | + |
| 77 | + def substitute_to_z(self): |
| 78 | + # substitute all non Z joint transforms according to rules |
| 79 | + nchanges = 0 |
| 80 | + out = ETS() |
| 81 | + for e in self: |
| 82 | + if e.isjoint: |
| 83 | + out *= e |
| 84 | + else: |
| 85 | + # do a substitution |
| 86 | + if e.axis == 'Rx': |
| 87 | + new = ETS.ry(pi2) * ETS.rz(e.eta) * ETS.ry(-pi2) |
| 88 | + elif e.axis == 'Ry': |
| 89 | + new = ETS.rx(-pi2) * ETS.rz(e.eta) * ETS.rx(pi2) |
| 90 | + elif e.axis == 'tx': |
| 91 | + new = ETS.ry(pi2) * ETS.tz(e.eta) * ETS.ry(-pi2) |
| 92 | + elif e.axis == 'ty': |
| 93 | + new = ETS.rx(-pi2) * ETS.tz(e.eta) * ETS.rx(pi2) |
| 94 | + else: |
| 95 | + out *= e |
| 96 | + continue |
| 97 | + out *= new |
| 98 | + nchanges += 1 |
| 99 | + |
| 100 | + self.data = out.data |
| 101 | + return nchanges |
| 102 | + |
| 103 | + def merge(self): |
| 104 | + |
| 105 | + def canmerge(prev, this): |
| 106 | + return prev.axis == this.axis and not (prev.isjoint and this.isjoint) |
| 107 | + |
| 108 | + out = ETS() |
| 109 | + while len(self.data) > 0: |
| 110 | + this = self.pop(0) |
| 111 | + |
| 112 | + if len(self.data) > 0: |
| 113 | + next = self[0] |
| 114 | + |
| 115 | + if canmerge(this, next): |
| 116 | + new = DHFactor.add(this, next) |
| 117 | + out *= new |
| 118 | + self.pop(0) # remove next from the queue |
| 119 | + print(f"Merging {this * next} to {new}") |
| 120 | + else: |
| 121 | + out *= this |
| 122 | + |
| 123 | + self.data = out.data |
| 124 | + # remove zeros |
| 125 | + |
| 126 | + @staticmethod |
| 127 | + def add(this, that): |
| 128 | + if this.isjoint and not that.isjoint: |
| 129 | + out = ETS(this) |
| 130 | + if out.eta is None: |
| 131 | + out.eta = that.eta |
| 132 | + else: |
| 133 | + out.eta += that.eta |
| 134 | + elif not this.isjoint and that.isjoint: |
| 135 | + out = ETS(that) |
| 136 | + if out.eta is None: |
| 137 | + out.eta = this.eta |
| 138 | + else: |
| 139 | + out.eta += this.eta |
| 140 | + else: |
| 141 | + raise ValueError('both ET cannot be joint variables') |
| 142 | + return out |
| 143 | + |
| 144 | + def eliminate_y(self): |
| 145 | + |
| 146 | + nchanges = 0 |
| 147 | + out = ETS() |
| 148 | + jointyet = False |
| 149 | + |
| 150 | + def eliminate(prev, this): |
| 151 | + if this.isjoint or prev.isjoint: |
| 152 | + return None |
| 153 | + |
| 154 | + new = None |
| 155 | + if prev.axis == 'Rx' and this.axis == 'ty': # RX.TY -> TZ.RX |
| 156 | + new = ETS.tx(prev.eta) * prev |
| 157 | + elif prev.axis == 'Rx' and this.axis == 'tz': # RX.TZ -> TY.RX |
| 158 | + new = ETS.ty(-prev.eta) * prev |
| 159 | + elif prev.axis == 'Ry' and this.axis == 'tz': # RY.TX-> TZ.RY |
| 160 | + new = ETS.tz(-prev.eta) * prev |
| 161 | + elif prev.axis == 'Ry' and this.axis == 'tz': # RY.TZ-> TX.RY |
| 162 | + new = ETS.tx(prev.eta) * prev |
| 163 | + |
| 164 | + elif prev.axis == 'ty' and this.axis == 'Rx': # TY.RX -> RX.TZ |
| 165 | + new = this * ETS.tz(-this.eta) |
| 166 | + elif prev.axis == 'tx' and this.axis == 'Rz': # TX.RZ -> RZ.TY |
| 167 | + new = this * ETS.tz(this.eta) |
| 168 | + elif prev.axis == 'Ry' and this.axis == 'Rx': # RY(Q).RX -> RX.RZ(-Q) |
| 169 | + new = this * ETS.Rz(-prev.eta) |
| 170 | + elif prev.axis == 'Rx' and this.axis == 'Ry': # RX.RY -> RZ.RX |
| 171 | + new = ETS.Rz(this.eta) * prev |
| 172 | + elif prev.axis == 'Rz' and this.axis == 'Rx': # RZ.RX -> RX.RY |
| 173 | + new = this * ETS.Ry(this.eta) |
| 174 | + return new |
| 175 | + |
| 176 | + for i in range(len(self)): |
| 177 | + this = self[i] |
| 178 | + jointyet = this.isjoint |
| 179 | + if i == 0 or not jointyet: |
| 180 | + continue |
| 181 | + |
| 182 | + prev = self[i-1] |
| 183 | + |
| 184 | + new = eliminate(prev, this) |
| 185 | + if new is not None: |
| 186 | + self[i-1:i] = new |
| 187 | + nchanges += 1 |
| 188 | + |
| 189 | + return nchanges |
| 190 | + |
| 191 | + def __str__(self, q=None): |
| 192 | + """ |
| 193 | + Pretty prints the ETS |
| 194 | +
|
| 195 | + :param q: control how joint variables are displayed |
| 196 | + :type q: str |
| 197 | + :return: Pretty printed ETS |
| 198 | + :rtype: str |
| 199 | +
|
| 200 | + ``q`` controls how the joint variables are displayed: |
| 201 | +
|
| 202 | + - None, format depends on number of joint variables |
| 203 | + - one, display joint variable as q |
| 204 | + - more, display joint variables as q0, q1, ... |
| 205 | + - if a joint index was provided, use this value |
| 206 | + - "", display all joint variables as empty parentheses ``()`` |
| 207 | + - "θ", display all joint variables as ``(θ)`` |
| 208 | + - format string with passed joint variables ``(j, j+1)``, so "θ{0}" |
| 209 | + would display joint variables as θ0, θ1, ... while "θ{1}" would |
| 210 | + display joint variables as θ1, θ2, ... ``j`` is either the joint |
| 211 | + index, if provided, otherwise a sequential value. |
| 212 | +
|
| 213 | + Example: |
| 214 | +
|
| 215 | + .. runblock:: pycon |
| 216 | +
|
| 217 | + >>> from roboticstoolbox import ETS |
| 218 | + >>> e = ETS.rz() * ETS.tx(1) * ETS.rz() |
| 219 | + >>> print(e[:2]) |
| 220 | + >>> print(e) |
| 221 | + >>> print(e.__str__("")) |
| 222 | + >>> print(e.__str__("θ{0}")) # numbering from 0 |
| 223 | + >>> print(e.__str__("θ{1}")) # numbering from 1 |
| 224 | + >>> # explicit joint indices |
| 225 | + >>> e = ETS.rz(j=3) * ETS.tx(1) * ETS.rz(j=4) |
| 226 | + >>> print(e) |
| 227 | + >>> print(e.__str__("θ{0}")) |
| 228 | +
|
| 229 | + .. note:: Angular parameters are converted to degrees, except if they |
| 230 | + are symbolic. |
| 231 | +
|
| 232 | + Example: |
| 233 | +
|
| 234 | + .. runblock:: pycon |
| 235 | +
|
| 236 | + >>> from roboticstoolbox import ETS |
| 237 | + >>> from spatialmath.base import symbol |
| 238 | + >>> theta, d = symbol('theta, d') |
| 239 | + >>> e = ETS.rx(theta) * ETS.tx(2) * ETS.rx(45, 'deg') * \ |
| 240 | + >>> ETS.ry(0.2) * ETS.ty(d) |
| 241 | + >>> str(e) |
| 242 | +
|
| 243 | + :SymPy: supported |
| 244 | + """ |
| 245 | + |
| 246 | + # override this class from ETS |
| 247 | + es = [] |
| 248 | + j = 0 |
| 249 | + c = 0 |
| 250 | + |
| 251 | + if q is None: |
| 252 | + if len(self.joints()) > 1: |
| 253 | + q = "q{0}" |
| 254 | + else: |
| 255 | + q = "q" |
| 256 | + |
| 257 | + # For et in the object, display it, data comes from properties |
| 258 | + # which come from the named tuple |
| 259 | + for et in self: |
| 260 | + |
| 261 | + s = f"{et.axis}(" |
| 262 | + if et.isjoint: |
| 263 | + if q is not None: |
| 264 | + if et.jindex is None: |
| 265 | + _j = j |
| 266 | + else: |
| 267 | + _j = et.jindex |
| 268 | + qvar = q.format(_j, _j+1) # lgtm [py/str-format/surplus-argument] # noqa |
| 269 | + else: |
| 270 | + qvar = "" |
| 271 | + if et.isflip: |
| 272 | + s += f"-{qvar}" |
| 273 | + else: |
| 274 | + s += f"{qvar}" |
| 275 | + j += 1 |
| 276 | + |
| 277 | + if et.eta is not None: |
| 278 | + if et.isrevolute: |
| 279 | + if base.issymbol(et.eta): |
| 280 | + if s[-1] == "(": |
| 281 | + s += f"{et.eta}" |
| 282 | + else: |
| 283 | + # adding to a previous value |
| 284 | + if str(et.eta).startswith('-'): |
| 285 | + s += f"{et.eta}" |
| 286 | + else: |
| 287 | + s += f"+{et.eta}" |
| 288 | + else: |
| 289 | + s += f"{et.eta * 180 / np.pi:.4g}°" |
| 290 | + |
| 291 | + elif et.isprismatic: |
| 292 | + s += f"{et.eta}" |
| 293 | + |
| 294 | + elif et.isconstant: |
| 295 | + s += f"C{c}" |
| 296 | + c += 1 |
| 297 | + s += ")" |
| 298 | + |
| 299 | + es.append(s) |
| 300 | + |
| 301 | + return " * ".join(es) |
| 302 | + |
| 303 | +if __name__ == "__main__": # pragram: no cover |
| 304 | + s = 'Rz(45) Tz(L1) Rz(q1) Ry(q2) Ty(L2) Tz(L3) Ry(q3) Tx(L4) Ty(L5) Tz(L6) Rz(q4) Ry(q5) Rz(q6)' |
| 305 | + |
| 306 | + ets = DHFactor(s) |
| 307 | + print(ets) |
| 308 | + ets.substitute_to_z() |
| 309 | + print(ets) |
| 310 | + ets.merge() |
| 311 | + print(ets) |
0 commit comments