org.dart.matlab.MatlabExpression.java Source code

Java tutorial

Introduction

Here is the source code for org.dart.matlab.MatlabExpression.java

Source

/*
 * Copyright (c) 2004-2010 The Regents of the University of California.
 * All rights reserved.
 *
 * '$Author: welker $'
 * '$Date: 2010-05-05 22:21:26 -0700 (Wed, 05 May 2010) $' 
 * '$Revision: 24234 $'
 * 
 * Permission is hereby granted, without written agreement and without
 * license or royalty fees, to use, copy, modify, and distribute this
 * software and its documentation for any purpose, provided that the above
 * copyright notice and the following two paragraphs appear in all copies
 * of this software.
 *
 * IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
 * FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
 * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
 * THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 * THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
 * PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
 * CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
 * ENHANCEMENTS, OR MODIFICATIONS.
 *
 */

package org.dart.matlab;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.StringTokenizer;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import ptolemy.actor.TypedAtomicActor;
import ptolemy.actor.TypedIOPort;
import ptolemy.actor.gui.style.TextStyle;
import ptolemy.actor.parameters.PortParameter;
import ptolemy.data.ArrayToken;
import ptolemy.data.BooleanToken;
import ptolemy.data.DoubleToken;
import ptolemy.data.IntToken;
import ptolemy.data.LongToken;
import ptolemy.data.StringToken;
import ptolemy.data.Token;
import ptolemy.data.UnsignedByteToken;
import ptolemy.data.expr.StringParameter;
import ptolemy.data.type.ArrayType;
import ptolemy.data.type.BaseType;
import ptolemy.kernel.CompositeEntity;
import ptolemy.kernel.util.IllegalActionException;
import ptolemy.kernel.util.NameDuplicationException;

//////////////////////////////////////////////////////////////////////////
//// MatlabExpression
/**
 * <p>
 * Allows the user to input a matlab script to be executed when the actor fires.
 * The following actor takes into consideration differnt startup routines for
 * MatlabSoftware for Unix/Windows os.
 * </p>
 * <p>
 * Input ports can be added and are automatically loaded into variables in
 * matlab which can be referenced by the port name.
 * </p>
 * <p>
 * Similarly output can be made by adding output ports to the actor. The output
 * values are taken from variables with the same names as the output ports.
 * </p>
 * <p>
 * <B>NOTE</B>: windows is a bit more tempermental than unix system. the EXE
 * file must be directly pointed to by the mlCmd property. E.g
 * c:\\matlab7\\bin\\win32\\matlab.exe
 * </p>
 * <p>
 * Also, windows command line matlab doesn't use the standard in and out,
 * instead it uses it's own command window, which makes it impossible to read
 * and write to the matlab process using the process input and output streams.
 * So instead the actor writes the data to a file and read in the outputs from
 * the file. The file is created with a random integer at the end of the file
 * name to (in theory) allow multiple matlab actors to run at the same time. The
 * file is deleted once it's been read.
 * </p>
 * <p>
 * <B>TODO</B>: currently this actor only works with standard single value and
 * array results. support for all forms of matlab output needs to be implemented
 * </p>
 * <p>
 * <B>NOTE</B>: now with java 1.4 complience!! using the ProcessBuilder in java
 * 1.5 makes things a lot easier but since we need to compile under 1.4 here it
 * is.
 * </p>
 * <p>
 * <B>Changelog 27/04/06</B>: * check for existance of executable under windows
 * * kill matlab process when stop() is called * check to make sure variable for
 * output port name exists if it doesn't, then set it to 0 * changed expression
 * to PortParameter * changed error handling: sends error message to output port
 * for some things
 * </p>
 * 
 * @author Tristan King, Nandita Mangal
 * @version $Id: MatlabExpression.java 24234 2010-05-06 05:21:26Z welker $
 * 
 */
public class MatlabExpression extends TypedAtomicActor {

    public MatlabExpression(CompositeEntity container, String name)
            throws NameDuplicationException, IllegalActionException {
        super(container, name);

        expression = new PortParameter(this, "expression");
        // make a text style, so that the parameter has multiple lines
        new TextStyle(expression, "Matlab Expression");
        // set the default script
        expression.setExpression("Enter Matlab function or script here...");
        // set to string mode, so the parameter doesn't have to have surrounding
        // ""s
        expression.setStringMode(true);

        output = new TypedIOPort(this, "output", false, true);
        output.setTypeEquals(BaseType.STRING);

        mlCmd = new StringParameter(this, "mlCmd");
        mlCmd.setDisplayName("Matlab Executable");
        // set default for specific os such as
        // "c:\\matlab7\\bin\\win32\\matlab.exe"
        mlCmd.setExpression("matlab");

        triggerSwitch = new TypedIOPort(this, "triggerSwitch", true, false);

    }

    // /////////////////////////////////////////////////////////////////
    // // logging variables ////

    private static final Log log = LogFactory.getLog("org.dart.matlab.MatlabExpression");
    // private static final boolean isDebugging = log.isDebugEnabled();

    // /////////////////////////////////////////////////////////////////
    // // ports and parameters ////

    /**
     * The output port. Outputs the matlab results.
     */
    public TypedIOPort output;

    /**
     * The expression that is evaluated : Matlab Function or Script from the
     * parameter dialog box or input port.
     */
    public PortParameter expression;

    /**
     * Path to matlab execuatble. defaults to "matlab"
     */
    public StringParameter mlCmd;

    /**
     * The trigger switch ,whether to execute the actor or not. (True or False /
     * 0 or 1(or more) as input enable the switch)
     */
    public TypedIOPort triggerSwitch;

    // /////////////////////////////////////////////////////////////////
    // // public methods ////
    public synchronized void fire() throws IllegalActionException {
        super.fire();
        boolean fireSwitch = true;

        if (triggerSwitch.getWidth() > 0) {
            Object inputSwitch = triggerSwitch.get(0);
            if (inputSwitch instanceof IntToken) {
                if (((IntToken) inputSwitch).intValue() >= 1) {
                    fireSwitch = true;
                } else {
                    fireSwitch = false;
                }

            } else if (inputSwitch instanceof BooleanToken) {

                if (((BooleanToken) inputSwitch).booleanValue()) {
                    fireSwitch = true;
                } else {
                    fireSwitch = false;
                }
            }

        }
        // if switch is set, then we execute actor
        // else do nothing.
        if (fireSwitch) {

            // get the script commands needed to pass into -r command
            String script = buildScript();

            String outputString = "";
            String randomFilename = tempFilename + "." + Math.abs((new Random()).nextInt());
            boolean isWindows = (System.getProperty("os.name").indexOf("Windows") > -1);

            // build the command array

            if (isWindows) {
                File f = new File(mlCmd.getExpression());
                if (!f.exists()) {
                    throw new IllegalActionException("Matlab process " + mlCmd.getExpression() + " doesn't exist");
                }
            }
            // TODO: same as above under unix

            // List argList = new ArrayList();
            String[] argList;

            // for some reason windows matlab doesn't work when supplied with
            // an array of arguments, and linux matlab doesn't work when
            // supplied
            // a single string with all the arguments.......
            if (isWindows) {
                argList = new String[1];
                argList[0] = mlCmd.getExpression() + " -r" + " \"" + script + "\"" + " -nodesktop" + " -nosplash"
                        + " -logfile " + randomFilename;
            } else {
                argList = new String[4];

                argList[0] = mlCmd.getExpression();
                argList[1] = "-nodesktop";
                argList[2] = "-nosplash" + " ";
                argList[3] = "-r" + " \"" + script + "\"";
            }

            // run the process and get output
            try {
                // Process p;
                if (isWindows) {

                    _p = Runtime.getRuntime().exec(argList[0]);
                } else {
                    _p = Runtime.getRuntime().exec(argList);
                }

                BufferedInputStream inputstream = null;
                int result = -1;

                // if we are running under windows
                if (isWindows) {
                    // wait for the process to end
                    result = _p.waitFor();
                    if (result == 0) {
                        // create the input stream from the log file created by
                        // matlab
                        inputstream = new BufferedInputStream(new FileInputStream(randomFilename));
                    }

                    // if not running under windows
                } else {
                    // get the processes input stream
                    inputstream = new BufferedInputStream(_p.getInputStream());
                }

                // read the stream until there is nothing left to read
                if (inputstream != null) {
                    while (true) {
                        int in;
                        try {
                            in = inputstream.read();
                        } catch (NullPointerException e) {
                            in = -1;
                        }
                        if (in == -1) {
                            break;
                        } else {
                            outputString += (char) in;
                        }
                    }

                    // close the input stream
                    inputstream.close();
                }

                if (isWindows) {
                    // delete the temp file. if it doesn't exist, then it will
                    // just return false
                    (new File(randomFilename)).delete();
                } else {
                    // make sure matlab exited OK.
                    result = _p.waitFor();
                }

                switch (result) {
                case 0:
                    break;
                case -113:
                    output.send(0, new StringToken("Matlab process was forcfully killed"));
                    return;
                case 129:
                    output.send(0, new StringToken("Matlab process was forcfully killed"));
                    return;
                default:
                    output.send(0, new StringToken(
                            "Matlab process returned \"" + result + "\".\nSomething must have gone wrong"));
                    return;
                }

            } catch (IOException e) {
                log.error("IOException: " + e.getMessage());
            } catch (InterruptedException e) {
                log.debug("interupted!");
            }

            // process the output from matlab
            parseOutput(outputString);
        }

    }

    public void stop() {
        if (_p != null) {
            _p.destroy();
        }
    }

    /**
     * builds the script for matlab to run
     * 
     * @return commands to run under matlab
     * @throws IllegalActionException
     */
    private String buildScript() throws IllegalActionException {
        List ipList = inputPortList();
        Iterator ipListIt = ipList.iterator();
        String inputs = "";

        while (ipListIt.hasNext()) {
            TypedIOPort tiop = (TypedIOPort) ipListIt.next();

            // if the input port is not the script itself.
            if (!(tiop.getName().equals("expression")) && !(tiop.getName().equals("triggerSwitch"))) {
                // get the token waiting on the port
                Token[] token = new Token[1];

                // TODO: check to make sure token exists
                token[0] = tiop.get(0);

                // setup the variable assignment
                // looks like: port_name = [ token_values ]
                inputs += tiop.getName() + " = ";
                inputs += "[" + getTokenValue(token) + "]\n";
            }
        }

        List opList = outputPortList();
        Iterator opListIt = opList.iterator();
        String outputs = "";

        while (opListIt.hasNext()) {
            TypedIOPort tiop = (TypedIOPort) opListIt.next();

            // make sure the output port found isn't output
            if (!tiop.equals(output)) {
                // add it to the list
                outputs += "if exist('" + tiop.getName() + "')," + tiop.getName() + ",else," + tiop.getName()
                        + "=0,end\n";
            }

        }

        // if there is a token waiting on the port input, grab it
        expression.update();

        String script = inputs + "sprintf('----')\n" + ((StringToken) expression.getToken()).stringValue() + "\n"
                + "sprintf('----')\n" + outputs + "quit\n";

        // should probably do this inside the code rather than here, but here
        // makes it easier to change later on.
        script = script.replaceAll("\n", ",");

        return script;
    }

    /**
     * parses the output from matlab to grab the values for the output ports
     * 
     * @param outputString
     * @throws IllegalActionException
     */
    private void parseOutput(String outputString) throws IllegalActionException {

        // this is a special token put in to make it easy to find the results
        // for ports
        final String scriptDivider = "ans =\n\n----";

        // process outputString
        // windows matlab writes '\r' characters along with the '\n' character.
        // remove them so the parsing script works properly.
        outputString = outputString.replaceAll("\r", "");

        // ensure indexof will work
        if (outputString.indexOf(scriptDivider) < 0) {
            throw new IllegalActionException("Error parsing output: Matlab must not have fired");
        }

        // cut off the proceeding matlab hello message
        String outs = outputString.substring(outputString.indexOf(scriptDivider) + scriptDivider.length(),
                outputString.length());

        String outputSendString = "";

        // send only the user entered results to the output port
        if (outs.indexOf(scriptDivider) >= 0) {
            outputSendString = outs.substring(0, outs.indexOf(scriptDivider));
        }
        output.send(0, new StringToken(outputSendString));

        // get all the results which are to be sent as tokens out of a specified
        // port
        String results = outs.substring(outs.indexOf(scriptDivider) + scriptDivider.length(), outs.length());

        // string tokenizer doesn't seem to beable to use '\n's as seperators
        // so to make tokenization easy, replace the combinations of '\n's with
        // a unique character.
        results = results.replaceAll("\n\n\n", "*");
        results = results.replaceAll("\n\n", "");

        // break the string up into seperate sections for each port result
        StringTokenizer st = new StringTokenizer(results, "*");
        while (st.hasMoreTokens()) {

            // break each result up into tokens to make port name and value easy
            // to extract
            String ssst = st.nextToken();

            StringTokenizer ist = new StringTokenizer(ssst);
            if (ist.countTokens() > 2) {
                String portName = ist.nextToken();
                String fss = ist.nextToken();
                if (fss.equals("Undefined") || !fss.equals("=")) {
                    // port is undefined, or something else has gone wrong
                    // TODO: should a nil token be passed?
                    // System.out.println("2nd token is \"" + fss + "\"");
                } else {
                    // we are good to continue
                    String[] value = new String[ist.countTokens()];
                    int count = 0;
                    while (ist.hasMoreTokens()) {
                        value[count++] = ist.nextToken();
                    }

                    // set the value of the output port
                    setOutputToken(portName, value);
                }
            }
        }
    }

    /**
     * recursive function to build a value for assignment to a matlab variable
     * from an input token.
     * 
     * recursively dives into arraytokens, surrounding each array with [ ]
     * brackets to conform with matlab array inputs
     * 
     * TODO: extend for more input types, e.g. MatrixTokens
     * 
     * @param token
     *     * @throws IllegalActionException
     */
    private String getTokenValue(Token[] token) throws IllegalActionException {

        String returnval = "";

        for (int i = 0; i < token.length; i++) {
            if (token[i].getType().isCompatible(new ArrayType(BaseType.UNKNOWN))) {
                returnval += " [ " + getTokenValue(((ArrayToken) token[i]).arrayValue()) + " ] ";
            } else if (token[i].getType().equals(BaseType.STRING)) {
                returnval += " '" + ((StringToken) token[i]).stringValue() + "' ";
            } else if (token[i].getType().equals(BaseType.INT)) {
                returnval += " " + ((IntToken) token[i]).intValue() + " ";
            } else if (token[i].getType().equals(BaseType.DOUBLE)) {
                returnval += " " + ((DoubleToken) token[i]).doubleValue() + " ";
            } else if (token[i].getType().equals(BaseType.BOOLEAN)) {
                returnval += " " + ((BooleanToken) token[i]).toString() + " ";
            } else if (token[i].getType().equals(BaseType.LONG)) {
                returnval += " " + ((LongToken) token[i]).longValue() + " ";
            } else if (token[i].getType().equals(BaseType.UNSIGNED_BYTE)) {
                returnval += " " + ((UnsignedByteToken) token[i]).byteValue() + " ";
            } else {
                throw new IllegalActionException("invalid token type: " + token[i].getType().toString());
            }
        }

        return returnval;
    }

    /**
     * sends a value out a specified port.
     * 
     * tries to figure out what data type the value is.
     * 
     * 
     * @param portName
     * @param value
     * @throws IllegalActionException
     */
    private void setOutputToken(String portName, String[] value) throws IllegalActionException {
        List opList = outputPortList();
        Iterator opListIt = opList.iterator();

        // make sure the portName isn't output
        if (portName.equals(output.getName())) {
            throw new IllegalActionException("sending a custom token out of port " + output.getName() + " is bad!");
        }

        // iterate through the list of ports
        while (opListIt.hasNext()) {
            TypedIOPort tiop = (TypedIOPort) opListIt.next();
            String thisPortName = tiop.getName();
            // check if the name is the same as the one we want to set
            if (thisPortName.equals(portName)) {

                // check the type of the array
                // type 2 = string > 1 = double > 0 = int > -1 = no value
                int type = -1;
                for (int i = 0; i < value.length; i++) {
                    try {
                        if (value[i].indexOf(".") > -1) {
                            // check if it can be converted to a double
                            Double.valueOf(value[i]);
                            // if the current type is an int then we can change
                            // the whole
                            // array type to double, but if the current type is
                            // a string
                            // then we have to keep it as a string
                            type = type > 1 ? type : 1;
                        } else {
                            // check if it can be converted to an int
                            Integer.valueOf(value[i]);
                            type = type > 0 ? type : 0;
                        }
                    } catch (NumberFormatException e) {
                        // it has to be a string
                        type = type > 2 ? type : 2;
                    }
                }

                // build the array using the specified type
                Token[] token;
                if (type == 2) {
                    token = new StringToken[value.length];
                    for (int i = 0; i < token.length; i++) {
                        token[i] = new StringToken(value[i]);
                    }
                    if (value.length > 1) {
                        tiop.setTypeEquals(new ArrayType(BaseType.STRING));
                    } else {
                        tiop.setTypeEquals(BaseType.STRING);
                    }
                } else if (type == 1) {
                    token = new DoubleToken[value.length];
                    for (int i = 0; i < token.length; i++) {
                        token[i] = new DoubleToken(Double.valueOf(value[i]).doubleValue());
                    }
                    if (value.length > 1) {
                        tiop.setTypeEquals(new ArrayType(BaseType.DOUBLE));
                    } else {
                        tiop.setTypeEquals(BaseType.DOUBLE);
                    }
                } else if (type == 0) {
                    token = new IntToken[value.length];
                    for (int i = 0; i < token.length; i++) {
                        token[i] = new IntToken(Integer.valueOf(value[i]).intValue());
                    }
                    if (value.length > 1) {
                        tiop.setTypeEquals(new ArrayType(BaseType.INT));
                    } else {
                        tiop.setTypeEquals(BaseType.INT);
                    }
                } else {
                    // throw an error if something went wrong
                    throw new IllegalActionException("invalid value passed for token");
                }

                // send the array over the output port
                if (token.length > 1) {
                    tiop.send(0, new ArrayToken(token));
                } else {
                    tiop.send(0, token, token.length);
                }

                break;
            }
        }
    }

    // /////////////////////////////////////////////////////////////////
    // // private variables ////

    private final static String tempFilename = "matlab_results";
    private Process _p;
}