import java.io.*; import java.util.*; import java.lang.*; /** * A class to simplify symbolic transform expressions. * @author Peter I. Corke peter.i.corke@gmail.com */ /** * Element is a class that represents one element in a transform expression. * The transform types represented include: * TX, TY, TZ pure translation along X, Y and Z axes respectively * RX, RY, RZ pure rotations about the X, Y and Z axes respectively * DH Denavit-Hartenberg joint transformation * * public boolean istrans() * public boolean isrot() * public int axis() * public boolean isjoint() { * public boolean factorMatch(int dhWhich, int i, int verbose) { * public void add(Element e) { * public Element(int type, int constant) { * public Element(int type) // new of specified type * * public static String toString(Element [] e) { * public String argString() { * public String toString() { * * Constructors: * Element(Element e) // clone of argument * Element(Element e, int type, int sign) // clone of argument with new type * Element(Element e, int type) // clone of argument with new type * Element(String s) */ class Element { static final int TX = 0, TY = 1, TZ = 2, RX = 3, RY = 4, RZ = 5, DH_STANDARD = 6, DH_MODIFIED = 7; // one of TX, TY ... RZ, DH_STANDARD/MODIFIED int type; // transform parameters, only one of these is set String var; // eg. q1, for joint var types String symconst; // eg. L1, for lengths int constant; // eg. 90, for angles // DH parameters, only set if type is DH_STANDARD/MODIFIED int theta, alpha; String A, D; int prismatic; int offset; // an array of counters for the application of each rule // just for debugging. static int[] rules = new int[20]; // class variable // mapping from type to string static final String[] typeName = { "Tx", "Ty", "Tz", "Rx", "Ry", "Rz", "DH", "DHm"}; // order of elementary transform for each DH convention // in each tuple, the first element is the transform type, // the second is true if it can be a joint variable. static final int dhStandard[][] = { {RZ, 1}, {TX, 0}, {TZ, 1}, {RX, 0} }; static final int dhModified[][] = { {RX, 0}, {TX, 0}, {RZ, 1}, {TZ, 1} }; /* * Display the number of times each rule was used in the * conversion. */ public static void showRuleUsage() { for (int i=0; i<20; i++) if (rules[i] > 0) System.out.println("Rule " + i + ": " + rules[i]); } // test if the Element is a translation, eg. TX, TY or TZ public boolean istrans() { return (type == TX) || (type == TY) || (type == TZ); } // test if the Element is a rotation, eg. RX, RY or RZ public boolean isrot() { return (type == RX) || (type == RY) || (type == RZ); } // true if this transform represents a joint coordinate, ie. not // a constant public boolean isjoint() { return this.var != null; } // return the axis, 0 for X, 1 for Y, 2 for Z public int axis() { switch (type) { case TX: case RX: return 0; case TY: case RY: return 1; case TZ: case RZ: return 2; default: throw new IllegalArgumentException("bad transform type"); } } // return the summation of two symbolic parameters as a string private String symAdd(String s1, String s2) { if ( (s1 == null) && (s2 == null) ) return null; else if ( (s1 != null) && (s2 == null) ) return new String(s1); else if ( (s1 == null) && (s2 != null) ) return new String(s2); else { return s1 + "+" + s2; } } /** * Add the argument of another Element to this element. * assumes that variable has not already been set * used by factor() to build a DH element */ public void add(Element e) { if ((this.type != DH_STANDARD) && (this.type != DH_MODIFIED)) throw new IllegalArgumentException("wrong element type " + this); System.out.println(" adding: " + this + " += " + e); switch (e.type) { case RZ: if (e.isjoint()) { this.prismatic = 0; this.var = e.var; this.offset = e.constant; this.theta = 0; } else this.theta = e.constant; break; case TX: this.A = e.symconst; break; case TZ: if (e.isjoint()) { this.prismatic = 1; this.var = e.var; this.D = null; } else this.D = e.symconst; break; case RX: this.alpha = e.constant; break; default: throw new IllegalArgumentException("cant factorize " + e); } } /* public void add(Element e) { if ((this.type != DH_STANDARD) && (this.type != DH_MODIFIED)) throw new IllegalArgumentException("wrong element type " + this); System.out.println(" adding: " + this + " += " + e); switch (e.type) { case RZ: this.theta = e.argString(); if (e.isjoint()) this.prismatic = 0; break; case TX: this.A = e.argString(); break; case TZ: this.D = e.argString(); if (e.isjoint()) this.prismatic = 1; break; case RX: this.alpha = e.argString(); break; default: throw new IllegalArgumentException("cant factorize " + e); } } */ // test if this particular element could be part of a DH term // eg. Rz(q1) can be, Rx(q1) cannot. public boolean factorMatch(int dhWhich, int i, int verbose) { int dhFactors[][]; boolean match; switch (dhWhich) { case DH_STANDARD: dhFactors = dhStandard; break; case DH_MODIFIED: dhFactors = dhModified; break; default: throw new IllegalArgumentException("bad DH type"); } match = (this.type == dhFactors[i][0]) && !((dhFactors[i][1] == 0) && this.isjoint()); if (verbose > 0) System.out.println(" matching " + this + " (i=" + i + ") " + " to " + typeName[dhFactors[i][0]] + "<" + dhFactors[i][1] + ">" + " -> " + match); return match; } /** * test if two transforms can be merged * @param e the element to compare with this * @return - this if no merge to be done * - null if the result is a null transform * - new transform resulting from a merge. */ Element merge(Element e) { /* * don't merge if dissimilar transform or * both are joint variables */ if ( (e.type != this.type) || (e.isjoint() && this.isjoint()) ) return this; Element sum = new Element(this); sum.var = symAdd(this.var, e.var); sum.symconst = symAdd(this.symconst, e.symconst); sum.constant = this.constant + e.constant; if (Math.abs(sum.constant) > 90) throw new IllegalArgumentException("rotation angle > 90"); /* * remove a null transform which can result from * a merge operation */ if ( !sum.isjoint() && (sum.symconst == null) && (sum.constant == 0)) { System.out.println("Eliminate: " + this + " " + e); return null; } else { System.out.println("Merge: " + this + " " + e + " := " + sum); return sum; } } /** * test if two transforms need to be swapped * @param e the element to compare with this * @return - true if swap is required */ boolean swap(Element next, int dhWhich) { /* * don't swap if both are joint variables */ if ( this.isjoint() && next.isjoint() ) return false; switch (dhWhich) { case Element.DH_STANDARD: /* * we want to sort terms into the order: * RZ * TX * TZ * RX */ /* TX TY TZ RX RY RZ */ int order[] = { 2, 0, 3, 4, 0, 1 }; if ( ((this.type == TZ) && (next.type == TX)) || /* * push constant translations through rotational joints * of the same type */ ((this.type == TX) && (next.type == RX) && next.isjoint()) || ((this.type == TY) && (next.type == RY)) && next.isjoint() || ((this.type == TZ) && (next.type == RZ)) && next.isjoint() || (!this.isjoint() && (this.type == RX) && (next.type == TX)) || (!this.isjoint() && (this.type == RY) && (next.type == TY)) || //(!this.isjoint() && (this.type == RZ) && (next.type == TZ)) || (!this.isjoint() && !next.isjoint() && (this.type == TZ) && (next.type == RZ)) || /* * move Ty terms to the right */ ((this.type == TY) && (next.type == TZ)) || ((this.type == TY) && (next.type == TX)) ) { System.out.println("Swap: " + this + " <-> " + next); return true; } break; case Element.DH_MODIFIED: if ( ((this.type == RX) && (next.type == TX)) || ((this.type == RY) && (next.type == TY)) || ((this.type == RZ) && (next.type == TZ)) || ((this.type == TZ) && (next.type == TX)) ) { System.out.println("Swap: " + this + " <-> " + next); return true; } break; default: throw new IllegalArgumentException("bad DH type"); } return false; } /** * Substitute this transform for a triple of transforms * that includes an RZ or TZ. * * @return - null if no substituion required * - array of Elements to substitute */ Element[] substituteToZ() { Element[] s = new Element[3]; switch (this.type) { case RX: s[0] = new Element(RY, 90); s[1] = new Element(this, RZ); s[2] = new Element(RY, -90); return s; case RY: s[0] = new Element(RX, -90); s[1] = new Element(this, RZ); s[2] = new Element(RX, 90); return s; case TX: s[0] = new Element(RY, 90); s[1] = new Element(this, TZ); s[2] = new Element(RY, -90); return s; case TY: s[0] = new Element(RX, -90); s[1] = new Element(this, TZ); s[2] = new Element(RX, 90); return s; default: return null; } } Element[] substituteToZ(Element prev) { Element[] s = new Element[3]; switch (this.type) { case RY: s[0] = new Element(RZ, 90); s[1] = new Element(this, RX); s[2] = new Element(RZ, -90); rules[8]++; return s; case TY: if (prev.type == RZ) { s[0] = new Element(RZ, 90); s[1] = new Element(this, TX); s[2] = new Element(RZ, -90); rules[6]++; return s; } else { s[0] = new Element(RX, -90); s[1] = new Element(this, TZ); s[2] = new Element(RX, 90); rules[7]++; return s; } default: return null; } } /** * Simple rewriting rule for adjacent transform pairs. Attempt to * eliminate TY and RY. * @param previous element in list * @return - null if no substituion required * - array of Elements to subsitute */ Element[] substituteY(Element prev, Element next) { Element[] s = new Element[2]; if (prev.isjoint() || this.isjoint()) return null; /* note that if rotation is -90 we must make the displacement -ve */ if ((prev.type == RX) && (this.type == TY)) { // RX.TY -> TZ.RX s[0] = new Element(this, TZ, prev.constant); s[1] = new Element(prev); rules[0]++; return s; } else if ((prev.type == RX) && (this.type == TZ)) { // RX.TZ -> TY.RX s[0] = new Element(this, TY, -prev.constant); s[1] = new Element(prev); rules[2]++; return s; } else if ((prev.type == RY) && (this.type == TX)) { // RY.TX-> TZ.RY s[0] = new Element(this, TZ, -prev.constant); s[1] = new Element(prev); rules[1]++; return s; } else if ((prev.type == RY) && (this.type == TZ)) { // RY.TZ-> TX.RY s[0] = new Element(this, TX, prev.constant); s[1] = new Element(prev); rules[11]++; return s; } else if ((prev.type == TY) && (this.type == RX)) { // TY.RX -> RX.TZ s[0] = new Element(this); s[1] = new Element(prev, TZ, -this.constant); rules[5]++; //return s; return null; } else if ((prev.type == RY) && (this.type == RX)) { // RY(Q).RX -> RX.RZ(-Q) s[0] = new Element(this); s[1] = new Element(prev, RZ, -1); rules[3]++; return s; } else if ((prev.type == RX) && (this.type == RY)) { // RX.RY -> RZ.RX s[0] = new Element(this, RZ); s[1] = new Element(prev); rules[4]++; return s; } else if ((prev.type == RZ) && (this.type == RX)) { // RZ.RX -> RX.RY s[0] = new Element(this); s[1] = new Element(prev, RY); //rules[10]++; //return s; return null; } return null; } /* * Element contructors. String is of the form: */ public Element(int type, int constant) { this.type = type; this.var = null; this.symconst = null; this.constant = constant; } public Element(int type) { // new of specified type this.type = type; } public Element(Element e) { // clone of argument this.type = e.type; if (e.var != null) this.var = new String(e.var); if (e.symconst != null) this.symconst = new String(e.symconst); this.constant = e.constant; } /** * Constructor for Element. * @param e Template for new Element. * @param type Replacement type for new Element. * @param sign Sign of argument, either -1 or +1. * @return a new Element with specified type and argument. */ public Element(Element e, int type, int sign) { // clone of argument with new type this.type = type; if (e.var != null) this.var = new String(e.var); this.constant = e.constant; if (e.symconst != null) this.symconst = new String(e.symconst); if (sign < 0) this.negate(); } public Element(Element e, int type) { // clone of argument with new type this(e, type, 1); } // negate the arguments of the element public void negate() { //System.out.println("negate: " + this.constant + " " + this.symconst); // flip the numeric part, easy this.constant = -this.constant; if (this.symconst != null) { StringBuffer s = new StringBuffer(this.symconst); // if no leading sign character insert one (so we can flip it) if ((s.charAt(0) != '+') && (s.charAt(0) != '-') ) s.insert(0, '+'); // go through the string and flip all sign chars for (int i=0; i= 6) throw(new IllegalArgumentException("bad transform name" + sType)); type = i; sRest = sRest.substring(1, sRest.length()-1); switch (sRest.charAt(0)) { case 'q': var = sRest; break; case 'L': symconst = sRest; break; default: try { constant = Integer.parseInt(sRest); } catch(NumberFormatException e) { System.err.println(e.getMessage()); throw(new IllegalArgumentException("bracket contents")); } } } // class method to convert Element vector to string /* public static String toString(Element [] e) { String s = ""; for (int i=0; i 0) s += "+" + offset; else if (offset < 0) s += offset; } else s += theta; s += ", "; // d if (prismatic > 0) s += var; else s += (D == null) ? "0" : D; s += ", "; // a s += (A == null) ? "0" : A; s += ", "; // alpha s += alpha; break; default: throw new IllegalArgumentException("bad Element type"); } return s; } /* * Return a string representation of the element. * eg. Rz(q1), Tx(L1), Rx(90), DH(....) */ public String toString() { String s = typeName[type] + "("; s += argString(); s += ")"; return s; } /* public String rotation() { } public String translation() { } */ } /********************************************************************** /* A list of Elements. Subclass of Java's arrayList * * public int factorize(int dhWhich, int verbose) * * public int floatRight() { * public int swap(int dhWhich) { * public int substituteToZ() { * public int substituteToZ2() { * public int substituteY() { * public int merge() { * public void simplify() { * public ElementList() { // constructor, use superclass * public String toString() { */ class ElementList extends ArrayList { /** * Attempt to group this and subsequent elements into a DH term * @return: the number of factors matched, zero means no DH term found * * Modifies the ElementList and compresses the terms. */ public int factorize(int dhWhich, int verbose) { int match, jvars; int i, j, f; Element e; int nfactors = 0; for (i=0; i= this.size()) break; e = (Element) this.get(j); if ((f == 0) && (verbose > 0)) System.out.println("Starting at " + e); if (e.factorMatch(dhWhich, f, verbose) ) { j++; // move on to next element match++; if (e.isjoint()) jvars++; if (jvars > 1) // can only have 1 joint var per DH break; } } if ((match == 0) || (jvars == 0)) continue; // no DH subexpression found, keep looking int start, end; if (verbose > 0) System.out.println(" found subexpression " + match + " " + jvars); start = i; end = j; if (jvars > 1) end--; Element dh = new Element(dhWhich); for (j=start; j 0) System.out.println(" result: " + dh); } return nfactors; } /** * Attempt to 'float' translational terms as far to the right as * possible and across joint boundaries. */ public int floatRight() { Element e, f = null; int nchanges = 0; int i, j; boolean crossed; for (i=0; i<(this.size()-1); i++) { e = (Element) this.get(i); if (e.isjoint()) continue; if (!e.istrans()) continue; f = null; crossed = false; for (j=i+1; j<(this.size()-1); j++) { f = (Element) this.get(j); if (f.istrans()) continue; if (f.isrot() && (f.axis() == e.axis())) { crossed = true; continue; } break; } if (crossed && (f != null)) { System.out.println("Float: " + e + " to " + f); this.remove(i); this.add(j-1, e); nchanges++; i--; } } return nchanges; } /** * Swap adjacent terms according to inbuilt rules so as to achieve * desired term ordering. */ public int swap(int dhWhich) { Element e; int total_changes = 0; int nchanges = 0; do { nchanges = 0; for (int i=0; i<(this.size()-1); i++) { e = (Element) this.get(i); if (e.swap( (Element) this.get(i+1), dhWhich)) { this.remove(i); this.add(i+1, e); nchanges++; } } total_changes += nchanges; } while (nchanges > 0); return total_changes; } /** * substitute all non Z joint transforms according to rules. */ public int substituteToZ() { Element e; Element[] replacement; int nchanges = 0; for (int i=0; i 0) && (nloops++ < 10)); } public ElementList() { // constructur, use superclass super(); } public String toString() { String s = ""; for (int i=0; i= 0) return s.substring(i); i = s.indexOf("-"); if (i >= 0) return s.substring(i); } return "0"; } static String convertMatlab(String s) { if (s == null) return " 0"; return " " + s; } public String toMatlab(String robot) { String dh = "["; String offs = "["; String base = ""; String tool = ""; String theta, a, d, alpha; int dhSeenYet = 0; Element e; for (int i=0; i 0) xform = xform.substring(2); // assign this string to base or tool depending on // whether or not we've seen the DH terms go by if (dhSeenYet == 0) base = xform; else tool = xform; } } dh += "]"; offs += "]"; // build the matlab string s = "robot(" + dh + ", " + robot ; if (base.length() > 0) s += ", 'base', " + base; if (tool.length() > 0) s += ", 'tool', " + tool; s += ");"; return s; } */ } public class DHFactor { ElementList results; // Matlab callable constructor public DHFactor(String src) { results = parseString(src); if (!this.isValid()) System.out.println("DHFactor: error: Incomplete factorization, no DH equivalent found"); } private String angle(Element e) { return angle(e.constant); } public String toString() { return results.toString(); } public String display() { return results.toString(); } private String angle(int a) { if (a == 0) return "0"; else if (a == 90) return "pi/2"; else if (a == -90) return "-pi/2"; else throw new IllegalArgumentException("bad transform angle"); } private String el2matlab(int from, int to) { String xform = ""; int i; for (i=from; i 0) xform += "*"; switch (e.type) { case Element.RX: xform += "trotx(" + angle(e) + ")"; break; case Element.RY: xform += "troty(" + angle(e) + ")"; break; case Element.RZ: xform += "trotz(" + angle(e) + ")"; break; case Element.TX: xform += "transl(" + e.symconst + ",0,0)"; break; case Element.TY: xform += "transl(0, " + e.symconst + ",0)"; break; case Element.TZ: xform += "transl(0,0," + e.symconst + ")"; break; } } if (xform.length() == 0) xform = "eye(4,4)"; return xform; } /* * Create a Toolbox legacy DH matrix. The column order is: * * theta d a alpha */ public String dh() { String s = "["; String theta, d; Element e; for (int i=0; i= 0) { // we've seen a DH factor before if ((i-iprev) > 1) { // but it was too long ago, fail! return false; } } iprev = i; // note where we saw it }; } return true; } public String offset() { String s = "["; Element e; for (int i=0; i=0; i--) { Element e = (Element)results.get(i); if ( (e.type == Element.DH_STANDARD) || (e.type == Element.DH_MODIFIED) ) return el2matlab(i, results.size()); } return "eye(4,4)"; } // return Matlab Toolbox robot creation command public String command(String name) { if (this.isValid()) return "SerialLink(" + this.dh() + ", 'name', '" + name + "', 'base', " + this.base() + ", 'tool', " + this.tool() + ", 'offset', " + this.offset() + ")"; else return "error('incompletely factored transform string')"; } public static ElementList parseFile(String filename) { BufferedReader src; String buffer; try { File file = new File(filename); if (!file.canRead() || !file.isFile()) throw new IOException("dh: file access/type error"); src = new BufferedReader(new FileReader(file)); // read the file and parse it src = new BufferedReader(new FileReader(file)); buffer = src.readLine(); return parseString(buffer); } catch (FileNotFoundException e) { System.err.println(e.getMessage()); System.exit(1); } catch (IOException e) { System.err.println(e.getMessage()); System.exit(1); } return null; } public static ElementList parseString(String buffer) { ElementList l = new ElementList(); try { System.out.println(buffer); StringTokenizer tokens = new StringTokenizer(buffer, " *."); while (tokens.hasMoreTokens()) l.add( new Element(tokens.nextToken()) ); System.out.println(l); l.simplify(); System.out.println(l); l.factorize(Element.DH_STANDARD, 0); System.out.println(l); return l; } catch (IllegalArgumentException e) { System.err.println(e.getMessage()); System.exit(1); } return null; } // command line instantiation // dhfactor file // dhfactor < stdin public static void main(String args[]) { if (args.length > 0) { ElementList l = parseFile(args[0]); System.err.println( l ); } else { System.err.println("no file name specified\n"); Element.showRuleUsage(); System.exit(1); } } }