All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.lsmp.djep.mrpe.MRpEval Maven / Gradle / Ivy

Go to download

JEP is a Java library for parsing and evaluating mathematical expressions.

The newest version!
/* @author rich
 * Created on 14-Apr-2004
 */
package org.lsmp.djep.mrpe;
import org.nfunk.jep.*;
import org.nfunk.jep.function.*;
import org.lsmp.djep.matrixJep.nodeTypes.*;
import org.lsmp.djep.matrixJep.*;
import org.lsmp.djep.vectorJep.*;
import org.lsmp.djep.vectorJep.values.*;
import org.lsmp.djep.xjep.*;
import java.util.*;
/**
 * A fast evaluation algorithm for equations using Vectors and Matrix over the Doubles.
 * This is based around reverse polish notation (hence the name M Rp Eval)
 * and is optimised for speed at every opportunity.
 * 

* To use do *

 * MatrixJep j = ...;
 * Node node = ...; 
 * MRpEval rpe = new MRpEval(j);
 * MRpCommandList list = rpe.compile(node);
 * MRpRes rpRes = rpe.evaluate(list);
 * System.out.println(rpRes.toString());
 * MatrixValueI mat = rpRes.toVecMat();
 * rpe.cleanUp();
 * 
* *

* The real use of this class is when an equation (or set of equations) * need to be repeatedly evaluated with different values for the variables. * MRpEval use an internal store for variable values different from those * used in the main Jep classes. Changes in the Jep variable values, * say by calling {@link org.nfunk.jep.JEP#setVarValue JEP.setVarValue}, * are reflected * in changes in MRpEval variables, (the converse does not hold). * A more efficient way is to use int ref=getVarRef(var) * to return an index number of the variable and then calling * setVarVal(ref,value) to set its value. * For example *

 * MRpCommandList list = rpe.compile(node);
 * int ref = rpe.getVarRef(j.getVar("x"));
 * for(double x=-1.;x<1.001;x+=0.1) {
 *      rpe.setVarVal(ref,x);
 *      rpe.evaluate(list);
 * }
 * 
* *

* Combining mrpe with differentation requires special techniques * to cope with that fact that internal equations are used *

* The compile methods converts the expression represented by node * into a string of commands. For example the expression "1+2*3" will * be converted into the sequence of commands *

 * Constant no 1 (pushes constant onto stack)
 * Constant no 2
 * Constant no 3
 * Multiply scalers (multiplies last two entries on stack)
 * Add scalers (adds last two entries on stack)
 * 
* The evaluate method executes these methods sequentially * using a stack (actually a set of stacks) * and returns the last object on the stack. *

* A few cautionary notes: the values returned by evaluate * are references to internal variables, their values will change * at the next call to compile or evaluate. * Its very unlikely to be thread safe. It only works over doubles; * expressions with complex numbers or strings will cause problems. * It is tuned to work best for expressions involving scalers and 2, 3 and 4 dimensional vectors and matricies, * larger vectors and matrices will be noticeably slower. * The cleanUp function should be used when you no longer need * the evaluator, this stops the evaluator listening to Variable * through the java.util.Observer interface. *

* Implementation notes * A lot of things have been done to make it as fast as possible: *

    *
  • Everything is final which maximises the possibility for in-lining.
  • *
  • All object creation happens during compile.
  • *
  • All calculations done using double values.
  • *
  • Vectors and Matrices are instances of VecObj and MatObj optimised for speed. * For instance a 2 by 2 matrix is an instance of Mat22Obj whose elements * are represented by the fields a,b,c,d. This eliminates bound checking on arrays. *
  • *
  • Each possible vector and matrix operation has been hand coded, and there are * a lot of methods (27 just for matrix multiplication!).
  • *
  • The values of variables are kept on local arrays for fast access. * These values are kept in sync with the main Jep Variables by using * the java.util.Observer interface.
  • *
* *

* For each type of vector or matrix (i.e. 2D vecs, 3D vecs, 4D vecs, 2 by 2 matrices ... 4 by 4 matrices. * there is a corresponding class V2Obj, M22Obj etc. * which stores the values and another class V2Store, M22Store etc. * Each Store class contains a stack, a heap and a array of variable values. * During evaluation objects are pushed and popped from the stack * when a new object is needed it is taken from the heap. * The operation is illustrated by the add method for 2 by 2 matrices. *

 * private final class M22Store
 * {
 *  ....
 *  final void add(){
 *   M22Obj r = stack[--sp]; // pop from stack
 *   M22Obj l = stack[--sp]; // pop from stack
 *	 M22Obj res = heap[hp++]; // result is next entry in heap
 *	 res.a = l.a+r.a;	// add each componant
 *	 res.b = l.b+r.b;
 *	 res.c = l.c+r.c;
 *	 res.d = l.d+r.d;
 *	 stack[sp++]=res;	// push result onto stack
 *  }
 * }
 * 
* * @author Rich Morris * Created on 14-Apr-2004 */ public final class MRpEval implements ParserVisitor { private MatrixOperatorSet opSet; public MRpEval(MatrixJep mjep) { this.opSet = (MatrixOperatorSet) mjep.getOperatorSet(); } private MRpEval() {} /** compile an expression of the type var = node. */ public final MRpCommandList compile(MatrixVariableI var,Node node) throws ParseException { MRpCommandList list = compile(node); ObjStore store = getStoreByDim(var.getDimensions()); short vRef = (short) store.addVar(var); store.decStack(); list.addCommand(ASSIGN,getDimType(var.getDimensions()),vRef); return list; } /** * Compile the expressions to produce a set of commands in reverse Polish notation. */ public final MRpCommandList compile(Node node) throws ParseException { curCommandList = new MRpCommandList(); node.jjtAccept(this,null); scalerStore.alloc(); v2Store.alloc(); v3Store.alloc(); v4Store.alloc(); vnStore.alloc(); m22Store.alloc(); m23Store.alloc(); m24Store.alloc(); m32Store.alloc(); m33Store.alloc(); m34Store.alloc(); m42Store.alloc(); m43Store.alloc(); m44Store.alloc(); mnnStore.alloc(); Dimensions dims = ((MatrixNodeI) node).getDim(); curCommandList.setFinalType(getDimType(dims)); // returnObj = Tensor.getInstance(dims); // if(dims.is2D()) // returnMat = (Matrix) returnObj; return curCommandList; } /** Index for each command */ static final short CONST = 0; static final short VAR = 1; static final short ADD = 2; static final short SUB = 3; static final short MUL = 4; static final short DIV = 5; static final short MOD = 6; static final short POW = 7; static final short AND = 8; static final short OR = 9; static final short NOT = 10; static final short LT = 11; static final short LE = 12; static final short GT = 13; static final short GE = 14; static final short NE = 15; static final short EQ = 16; static final short LIST = 17; static final short DOT = 18; static final short CROSS = 19; static final short ASSIGN = 20; static final short VLIST = 21; static final short MLIST = 22; static final short FUN = 23; static final short UMINUS = 24; /** Constant type scalers - used in the aux field of RpCommand */ private static final short SCALER = 0; // Scalers private static final short V2 = 2; // 2D vect private static final short V3 = 3; private static final short V4 = 4; private static final short Vn = 5; // n D vec private static final short M22 = 6; // 2 by 2 mat private static final short M23 = 7; // 2 by 3 mat private static final short M24 = 8; private static final short M32 = 9; private static final short M33 = 10; private static final short M34 = 11; private static final short M42 = 12; private static final short M43 = 13; private static final short M44 = 14; private static final short Mnn = 15; // other mats private static final short Dtens = 16; // tensors /** Standard functions **/ private static final short SIN = 1; private static final short COS = 2; private static final short TAN = 3; private static final short ASIN = 4; private static final short ACOS = 5; private static final short ATAN = 6; private static final short SINH = 7; private static final short COSH = 8; private static final short TANH = 9; private static final short ASINH = 10; private static final short ACOSH = 11; private static final short ATANH = 12; private static final short ABS = 13; private static final short EXP = 14; private static final short LOG = 15; private static final short LN = 16; private static final short SQRT = 17; private static final short SEC = 18; private static final short COSEC = 19; private static final short COT = 20; // 2 argument functions // private static final short ANGLE = 21; // private static final short MODULUS = 22; /** Hashtable for function name lookup **/ private static final Hashtable functionHash = new Hashtable(); { functionHash.put("sin",new Short(SIN)); functionHash.put("cos",new Short(COS)); functionHash.put("tan",new Short(TAN)); functionHash.put("asin",new Short(ASIN)); functionHash.put("acos",new Short(ACOS)); functionHash.put("atan",new Short(ATAN)); functionHash.put("sinh",new Short(SINH)); functionHash.put("cosh",new Short(COSH)); functionHash.put("tanh",new Short(TANH)); functionHash.put("asinh",new Short(ASINH)); functionHash.put("acosh",new Short(ACOSH)); functionHash.put("atanh",new Short(ATANH)); functionHash.put("abs",new Short(ABS)); functionHash.put("exp",new Short(EXP)); functionHash.put("log",new Short(LOG)); functionHash.put("ln",new Short(LN)); functionHash.put("sqrt",new Short(SQRT)); functionHash.put("sec",new Short(SEC)); functionHash.put("cosec",new Short(COSEC)); functionHash.put("cot",new Short(COT)); } /** Contains the constant values **/ private double constVals[] = new double[0]; /** * Finds the reference number used for this variable. * @param var * @return an index used to refer to the variable * @throws ParseException */ public int getVarRef(Variable var) throws ParseException { Dimensions dims = ((MatrixVariableI)var).getDimensions(); ObjStore store = getStoreByDim(dims); int ref = store.addVar((MatrixVariableI) var); return ref; } /** * Finds the reference number used for this variable. * @param var * @return an index used to refer to the variable * @throws ParseException */ public int getVarRef(MatrixVariableI var) throws ParseException { Dimensions dims = var.getDimensions(); ObjStore store = getStoreByDim(dims); int ref = store.addVar(var); return ref; } /** * Sets value of rpe variable. * * @param ref the reference number for the variable * (found using {@link #getVarRef(org.lsmp.djep.matrixJep.MatrixVariableI)}) * @param val * @throws ParseException */ public final void setVarValue(int ref,MatrixValueI val) throws ParseException { ObjStore store = getStoreByDim(val.getDim()); store.setVarValue(ref,val); } /** * Sets value of rpe variable. * Only applies to scaler (double variables). * * @param ref the reference number for the variable * (found using {@link #getVarRef(org.lsmp.djep.matrixJep.MatrixVariableI)}) * @param val the value */ public final void setVarValue(int ref,double val) { scalerStore.setVarValue(ref,val); } private final static class ScalerObj extends MRpRes { double a; private ScalerObj(double val) {a =val; } public final Dimensions getDims() { return Dimensions.ONE; } public final void copyToVecMat(MatrixValueI res) throws ParseException { if(! res.getDim().is0D()) throw new ParseException("CopyToVecMat: dimension of argument "+res.getDim()+" is not equal to dimension of object "+getDims()); res.setEle(0,new Double(a)); } public final String toString() { return String.valueOf(a); } public Object toArray() { return new double[]{a}; } } private ScalerObj scalerRes = new ScalerObj(0.0); private abstract static class VecObj extends MRpRes { public final void copyToVecMat(MatrixValueI res) throws ParseException { if(! getDims().equals(res.getDim())) throw new ParseException("CopyToVecMat: dimension of argument "+res.getDim()+" is not equal to dimension of object "+getDims()); copyToVec((MVector) res); } public abstract void copyToVec(MVector res); abstract double[] toArrayVec(); public Object toArray() { return toArrayVec(); } /** * Sets the value of th vector frm an array. */ // public abstract void fromArray(double array[]); } private abstract static class MatObj extends MRpRes { public final void copyToVecMat(MatrixValueI res) throws ParseException { if(! getDims().equals(res.getDim())) throw new ParseException("CopyToVecMat: dimension of argument "+res.getDim()+" is not equal to dimension of object "+getDims()); copyToMat((Matrix) res); } public abstract void copyToMat(Matrix res); abstract double[][] toArrayMat(); public Object toArray() { return toArrayMat(); } } /** * Base class for storage for each type of data. * Each subclass should define *
	 * private Obj stack[];
	 * private Obj heap[];
	 * private Obj vars[]= new Obj[0];
	 * 
* where Obj is an Object of the specific type, eg V2Obj. * Memory for the data is allocated from the heap * and the stack is the current data used for calculations. * Data for Variables is stored in vars and references to the Variables * in varRefs. */ private abstract static class ObjStore implements Observer { /** Contains references to Variables of this type */ Hashtable varRefs = new Hashtable(); /** The stack pointer */ int sp=0; /** Maximum size of stack */ int stackMax=0; /** The heap pointer */ int hp=0; final void incStack() {sp++; if(sp > stackMax) stackMax = sp; } final void incHeap() {hp++;} final void decStack() throws ParseException {--sp; if(sp <0 ) throw new ParseException("RPEval: stack error");} /** call this to reset pointers as first step in evaluation */ final void reset() { sp = 0; hp = 0; } /** Add a reference to this variable. * @return the index of variable in table */ final int addVar(MatrixVariableI var){ Object index = varRefs.get(var); if(index==null) { int size = varRefs.size(); expandVarArray(var); varRefs.put(var,new Integer(size)); copyFromVar(var,size); ((Variable) var).addObserver(this); return size; } return ((Integer) index).intValue(); } /** Callback function for Observable Variables. * Called whenever the value of a variable is changed * so the private list of variables is kept in sync. */ final public void update(Observable obs, Object arg1) { MatrixVariableI var = (MatrixVariableI) obs; Object index = varRefs.get(var); copyFromVar(var,((Integer) index).intValue()); } abstract public void setVarValue(int ref,MatrixValueI val); /** allocates space needed */ abstract void alloc(); /** removed store from list of listeners. */ final void cleanUp() { for(Enumeration e=varRefs.keys();e.hasMoreElements();) { Variable var = (Variable) e.nextElement(); var.deleteObserver(this); } varRefs.clear(); } /** Copy variable values into into private storage */ abstract void copyFromVar(MatrixVariableI var,int i); /** Copy values from private storage into JEP variables */ // abstract void copyToVar(MatrixVariableI var,int i); /** expand size of array used to hold variable values. */ abstract void expandVarArray(MatrixVariableI var); /** add two objects of same type */ abstract void add(); /** subtract two objects of same type */ abstract void sub(); /** subtract two objects of same type */ abstract void uminus(); /** multiply by a scaler either of left or right */ abstract void mulS(); /** convert a set of scaler values into object of this type */ abstract void makeList(); /** assign a variable to stack value * @param i index of variable */ abstract void assign(int i); } private final class ScalerStore extends ObjStore { double stack[]=new double[0]; double vars[]= new double[0]; final void alloc() { stack = new double[stackMax]; } final void expandVarArray(MatrixVariableI var) { double newvars[] = new double[vars.length+1]; System.arraycopy(vars,0,newvars,0,vars.length); vars = newvars; } final void copyFromVar(MatrixVariableI var,int i) { if(var.hasValidValue()) { Scaler val = (Scaler) var.getMValue(); vars[i]=val.doubleValue(); } } public void setVarValue(int ref, double val) { vars[ref] = val; } public final void setVarValue(int ref,MatrixValueI val) { vars[ref]=((Scaler) val).doubleValue(); } final void add(){ double r = stack[--sp]; stack[sp-1] += r; } final void sub(){ double r = stack[--sp]; stack[sp-1] -= r; } final void uminus(){ double r = stack[--sp]; stack[sp++] = -r; } final void mulS(){ double r = stack[--sp]; stack[sp-1] *= r; } final void divS(){ double r = stack[--sp]; stack[sp-1] /= r; } final void mod(){ double r = stack[--sp]; stack[sp-1] %= r; } final void pow(){ double r = stack[--sp]; double l = stack[--sp]; stack[sp++] = Math.pow(l,r); } final void powN(int n){ double r = stack[--sp]; switch(n){ case 0: r = 1.0; break; case 1: break; case 2: r *= r; break; case 3: r *= r*r; break; case 4: r *= r*r*r; break; case 5: r *= r*r*r*r; break; default: r = Math.pow(r,n); break; } stack[sp++] = r; } final void makeList() { } final void assign(int i) { vars[i] = stack[--sp]; ++sp; } final void and(){ double r = stack[--sp]; double l = stack[--sp]; if((l != 0.0) && (r != 0.0)) stack[sp++] = 1.0; else stack[sp++] = 0.0; } final void or(){ double r = stack[--sp]; double l = stack[--sp]; if((l != 0.0) || (r != 0.0)) stack[sp++] = 1.0; else stack[sp++] = 0.0; } final void not(){ double r = stack[--sp]; if(r == 0.0) stack[sp++] = 1.0; else stack[sp++] = 0.0; } final void lt(){ double r = stack[--sp]; double l = stack[--sp]; if(l < r) stack[sp++] = 1.0; else stack[sp++] = 0.0; } final void gt(){ double r = stack[--sp]; double l = stack[--sp]; if(l > r) stack[sp++] = 1.0; else stack[sp++] = 0.0; } final void le(){ double r = stack[--sp]; double l = stack[--sp]; if(l <= r) stack[sp++] = 1.0; else stack[sp++] = 0.0; } final void ge(){ double r = stack[--sp]; double l = stack[--sp]; if(l >= r) stack[sp++] = 1.0; else stack[sp++] = 0.0; } final void eq(){ double r = stack[--sp]; double l = stack[--sp]; if(l == r) stack[sp++] = 1.0; else stack[sp++] = 0.0; } final void neq(){ double r = stack[--sp]; double l = stack[--sp]; if(l != r) stack[sp++] = 1.0; else stack[sp++] = 0.0; } } ScalerStore scalerStore = new ScalerStore(); /** Base class for vector type storage */ private abstract class VecStore extends ObjStore { abstract void copyVar(int i,MVector val); final void copyFromVar(MatrixVariableI var,int i) { if(var.hasValidValue()) { MVector val = (MVector) ((MatrixVariable) var).getMValue(); copyVar(i,val); } } public final void setVarValue(int ref, MatrixValueI val) { copyVar(ref,(MVector) val); } } private static final class V2Obj extends VecObj { double a,b; private static Dimensions dims = Dimensions.TWO; public Dimensions getDims() { return dims; } public String toString() { return "["+a+","+b+"]"; } public void fromVec(MVector val){ a = ((Double) val.getEle(0)).doubleValue(); b = ((Double) val.getEle(1)).doubleValue(); } public void copyToVec(MVector val){ val.setEle(0,new Double(a)); val.setEle(1,new Double(b)); } public double[] toArrayVec() { return new double[]{a,b}; } } private final class V2Store extends VecStore { V2Obj stack[]; V2Obj heap[]; V2Obj vars[]= new V2Obj[0]; final void alloc() { heap = new V2Obj[hp]; for(int i=0;i0) sb.append(","); sb.append(data[i]); } sb.append("]"); return sb.toString(); } public void fromVec(MVector val){ for(int i=0;i=0;--i) res.data[i] = scalerStore.stack[--scalerStore.sp]; stack[sp++]=res; } final void makeList() { throw new UnsupportedOperationException("VnObj: makeList cannot be called with no arguments"); } final void assign(int j) { VnObj r = stack[sp-1]; VnObj res = vars[j]; for(int i=0;i0) sb.append(','); sb.append('['); for(int j=0;j0) sb.append(','); sb.append(data[i][j]); } sb.append(']'); } sb.append(']'); return sb.toString(); } public void fromMat(Matrix val){ for(int i=0;i=0;--i) for(int j=cols-1;j>=0;--j) res.data[i][j]= scalerStore.stack[--scalerStore.sp]; stack[sp++]=res; } final void makeList() { throw new UnsupportedOperationException("VnObj: makeList cannot be called with no arguments"); } final void assign(int k) { MnnObj r = stack[sp-1]; MnnObj res = vars[k]; for(int i=0;i=1) { leftnode = (MatrixNodeI) node.jjtGetChild(0); ldims = leftnode.getDim(); } if(nChild>=2) { rightnode = (MatrixNodeI) node.jjtGetChild(1); rdims = rightnode.getDim(); } if(mnode.isOperator()) { XOperator op = (XOperator) mnode.getOperator(); if(op.isBinary()) if(nChild!=2) throw new ParseException("RpeEval: binary operator must have two children, but it has "+nChild); if(op.isUnary()) if(nChild!=1) throw new ParseException("RpeEval: unary operator must have one child, but it has "+nChild); if(op == opSet.getAdd()) { if(!dims.equals(ldims) || !dims.equals(rdims)) throw new ParseException("RpeEval: dims for add must be equal"); curCommandList.addCommand(ADD,getDimType(dims)); decByDim(dims); incheapByDim(dims); return null; } else if(op == opSet.getSubtract()) { if(!dims.equals(ldims) || !dims.equals(rdims)) throw new ParseException("RpeEval: dims for add must be equal"); curCommandList.addCommand(SUB,getDimType(dims)); decByDim(dims); incheapByDim(dims); return null; } else if(op == opSet.getUMinus()) { curCommandList.addCommand(UMINUS,getDimType(dims)); incheapByDim(dims); return null; } else if(op == opSet.getMultiply()) { decByDim(rdims); decByDim(ldims); incByDim(dims); incheapByDim(dims); curCommandList.addCommand(MUL,getDimType(ldims),getDimType(rdims)); return null; } else if(op == opSet.getMList()) { incByDim(dims); incheapByDim(dims); for(int j=0;j operator must both be one"); scalerStore.incStack(); decByDim(ldims); decByDim(rdims); curCommandList.addCommand(GT,SCALER); return null; } else if(op == opSet.getLE()) { if(!ldims.is0D() || !rdims.is0D())throw new ParseException("Dimensions of operands for <= operator must both be one"); scalerStore.incStack(); decByDim(ldims); decByDim(rdims); curCommandList.addCommand(LE,SCALER); return null; } else if(op == opSet.getGE()) { if(!ldims.is0D() || !rdims.is0D())throw new ParseException("Dimensions of operands for >= operator must both be one"); scalerStore.incStack(); decByDim(ldims); decByDim(rdims); curCommandList.addCommand(GE,SCALER); return null; } else if(op == opSet.getAnd()) { if(!ldims.is0D() || !rdims.is0D())throw new ParseException("Dimensions of operands for && operator must both be one"); scalerStore.incStack(); decByDim(ldims); decByDim(rdims); curCommandList.addCommand(AND,SCALER); return null; } else if(op == opSet.getOr()) { if(!ldims.is0D() || !rdims.is0D())throw new ParseException("Dimensions of operands for || operator must both be one"); scalerStore.incStack(); decByDim(ldims); decByDim(rdims); curCommandList.addCommand(OR,SCALER); return null; } else if(op == opSet.getNot()) { if(!ldims.is0D())throw new ParseException("Dimension of operand for not operator must be one"); scalerStore.incStack(); decByDim(rdims); curCommandList.addCommand(NOT,SCALER); return null; } else if(op == opSet.getDivide()) { if(!rdims.is0D())throw new ParseException("RHS operands of / operator must be a Scaler"); decByDim(rdims); decByDim(ldims); incByDim(dims); incheapByDim(dims); curCommandList.addCommand(DIV,getDimType(ldims),getDimType(rdims)); return null; } else if(op == opSet.getMod()) { if(!ldims.is0D() || !rdims.is0D())throw new ParseException("Dimensions of operands for || operator must both be one"); scalerStore.incStack(); decByDim(ldims); decByDim(rdims); curCommandList.addCommand(MOD,SCALER); return null; } else if(op == opSet.getPower()) { if(!ldims.is0D() || !rdims.is0D())throw new ParseException("Dimensions of operands for || operator must both be one"); scalerStore.incStack(); decByDim(ldims); decByDim(rdims); curCommandList.addCommand(POW,SCALER); return null; } throw new ParseException("RpeEval: Sorry unsupported operator/function: "+ mnode.getName()); } // other functions Short val = (Short) functionHash.get(mnode.getName()); if(val == null) throw new ParseException("RpeEval: Sorry unsupported operator/function: "+ mnode.getName()); if(mnode.getPFMC().getNumberOfParameters() == 1 && nChild == 1) { scalerStore.incStack(); decByDim(ldims); curCommandList.addCommand(FUN,val.shortValue()); return null; } throw new ParseException("RpeEval: Sorry unsupported operator/function: "+ mnode.getName()); } /***************************** evaluation *****************************/ /** Evaluate the expression. * * @return the value after evaluation */ public final MRpRes evaluate(MRpCommandList comList) { scalerStore.reset(); v2Store.reset(); v3Store.reset(); v4Store.reset(); vnStore.reset(); m22Store.reset(); m23Store.reset(); m24Store.reset(); m32Store.reset(); m33Store.reset(); m34Store.reset(); m42Store.reset(); m43Store.reset(); m44Store.reset(); mnnStore.reset(); // Now actually process the commands int num = comList.getNumCommands(); for(short commandNum=0;commandNum




© 2015 - 2024 Weber Informatics LLC | Privacy Policy